aliases = new HashMap<>();
+ aliases.put("default", "MainActivityIconDefault");
+ aliases.put("midnight_circuit", "MainActivityIconMidnightCircuit");
+ aliases.put("aurora_pulse", "MainActivityIconAuroraPulse");
+ aliases.put("terminal_glow", "MainActivityIconTerminalGlow");
+ aliases.put("solar_flare", "MainActivityIconSolarFlare");
+ aliases.put("blueprint", "MainActivityIconBlueprint");
+ aliases.put("pixel_party", "MainActivityIconPixelParty");
+ APP_ICON_ALIASES = Collections.unmodifiableMap(aliases);
+ }
+
private Activity activity;
private Context context;
private int REQ_PERMISSIONS = 1;
@@ -159,6 +173,9 @@ public void run() {
}
);
return true;
+ case "set-app-icon":
+ setAppIcon(arg1, callbackContext);
+ return true;
case "get-cordova-intent":
getCordovaIntent(callbackContext);
return true;
@@ -2185,6 +2202,45 @@ private void setNativeContextMenuDisabled(boolean disabled) {
webView.setNativeContextMenuDisabled(disabled);
}
+ /**
+ * Dynamically changes the app icon by toggling activity-alias components.
+ *
+ * The launcher icon is always represented by an activity-alias (including
+ * the default icon) so that the running MainActivity component never has to
+ * be disabled. Disabling the currently running component would make Android
+ * force-stop/restart the app, so only aliases are toggled here.
+ *
+ * @param iconName Icon id (e.g. "midnight_circuit") or "default" to restore
+ * the original launcher icon.
+ * @param callback Callback invoked with the result.
+ */
+ private void setAppIcon(String iconName, CallbackContext callback) {
+ try {
+ String packageName = context.getPackageName();
+ PackageManager pm = context.getPackageManager();
+ String key = iconName == null ? "default" : iconName.toLowerCase();
+
+ if (!APP_ICON_ALIASES.containsKey(key)) {
+ callback.error("Unknown app icon: " + iconName);
+ return;
+ }
+
+ for (Map.Entry entry : APP_ICON_ALIASES.entrySet()) {
+ boolean enabled = entry.getKey().equals(key);
+ pm.setComponentEnabledSetting(
+ new ComponentName(packageName, packageName + "." + entry.getValue()),
+ enabled
+ ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP
+ );
+ }
+ callback.success();
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
private void extractAsset(
String assetName,
String destinationPath,
diff --git a/src/plugins/system/system.d.ts b/src/plugins/system/system.d.ts
index aa2ef8bf7..613104506 100644
--- a/src/plugins/system/system.d.ts
+++ b/src/plugins/system/system.d.ts
@@ -295,6 +295,13 @@ interface System {
onSuccess?: () => void,
onFail?: OnFail,
): void;
+ /**
+ * Change the app icon at runtime.
+ * @param iconName Icon id, e.g. "midnight_circuit", or "default" to restore the original icon
+ * @param onSuccess
+ * @param onFail
+ */
+ setAppIcon(iconName: string, onSuccess: OnSuccessBool, onFail: OnFail): void;
}
interface Window{
diff --git a/src/plugins/system/www/plugin.js b/src/plugins/system/www/plugin.js
index 8bebd47ef..c8722e7e4 100644
--- a/src/plugins/system/www/plugin.js
+++ b/src/plugins/system/www/plugin.js
@@ -231,6 +231,15 @@ module.exports = {
[String(!!disabled)]
);
},
+ /**
+ * Change the app icon at runtime.
+ * @param iconName Icon id, e.g. "midnight_circuit", or "default" to restore the original icon
+ * @param onSuccess
+ * @param onFail
+ */
+ setAppIcon: function (iconName, onSuccess, onFail) {
+ cordova.exec(onSuccess, onFail, 'System', 'set-app-icon', [iconName]);
+ },
getGlobalSetting: function (key, onSuccess, onFail) {
cordova.exec(onSuccess, onFail, 'System', 'get-global-setting', [key]);
},
diff --git a/src/settings/appSettings.js b/src/settings/appSettings.js
index a7f912f74..2817bb52f 100644
--- a/src/settings/appSettings.js
+++ b/src/settings/appSettings.js
@@ -6,11 +6,13 @@ import loader from "dialogs/loader";
import select from "dialogs/select";
import actions from "handlers/quickTools";
import actionStack from "lib/actionStack";
+import { getAppIconLabel } from "lib/appIcons";
import config from "lib/config";
import fonts from "lib/fonts";
import lang from "lib/lang";
import openFile from "lib/openFile";
import appSettings from "lib/settings";
+import appIconSetting from "pages/appIconSetting";
import FontManager from "pages/fontManager";
import QuickToolsSettings from "pages/quickTools";
import encodings, { getEncoding } from "utils/encodings";
@@ -64,6 +66,17 @@ export default function otherSettings() {
info: strings["settings-info-app-fullscreen"],
category: categories.interface,
},
+ {
+ key: "appIcon",
+ text: strings["app icon"] || "App icon",
+ value: values.appIcon || "default",
+ valueText: (value) => getAppIconLabel(value),
+ info:
+ strings["settings-info-app-icon"] ||
+ "Choose the app icon displayed on your device.",
+ category: categories.interface,
+ chevron: true,
+ },
{
key: "uiZoom",
text: strings["ui zoom"] || "UI zoom",
@@ -391,6 +404,20 @@ export default function otherSettings() {
FontManager();
return;
+ case "appIcon":
+ await appIconSetting();
+ {
+ const item = items.find((i) => i.key === "appIcon");
+ if (item) item.value = appSettings.value.appIcon || "default";
+ const $value = this.get(".setting-trailing-value");
+ if ($value) {
+ $value.textContent = getAppIconLabel(
+ appSettings.value.appIcon || "default",
+ );
+ }
+ }
+ return;
+
case "appFont":
await fonts.setAppFont(value);
break;
diff --git a/tests/unit/appIcons.test.js b/tests/unit/appIcons.test.js
new file mode 100644
index 000000000..77df9a005
--- /dev/null
+++ b/tests/unit/appIcons.test.js
@@ -0,0 +1,24 @@
+import { describe, expect, it } from "vitest";
+import { APP_ICONS, getAppIconLabel } from "lib/appIcons";
+
+describe("appIcons", () => {
+ it("exposes the default icon first", () => {
+ expect(APP_ICONS[0].id).toBe("default");
+ });
+
+ it("references an svg preview for each icon", () => {
+ for (const icon of APP_ICONS) {
+ expect(icon.image).toMatch(/\.svg$/);
+ }
+ });
+
+ describe("getAppIconLabel", () => {
+ it("returns the label for a known icon", () => {
+ expect(getAppIconLabel("midnight_circuit")).toBe("Midnight Circuit");
+ });
+
+ it("falls back to the default label for unknown icons", () => {
+ expect(getAppIconLabel("unknown_icon")).toBe("Default");
+ });
+ });
+});
diff --git a/www/icons/ic_acode_aurora_pulse.svg b/www/icons/ic_acode_aurora_pulse.svg
new file mode 100644
index 000000000..f188ae3dd
--- /dev/null
+++ b/www/icons/ic_acode_aurora_pulse.svg
@@ -0,0 +1,37 @@
+
diff --git a/www/icons/ic_acode_blueprint.svg b/www/icons/ic_acode_blueprint.svg
new file mode 100644
index 000000000..129975a8a
--- /dev/null
+++ b/www/icons/ic_acode_blueprint.svg
@@ -0,0 +1,38 @@
+
diff --git a/www/icons/ic_acode_default.svg b/www/icons/ic_acode_default.svg
new file mode 100644
index 000000000..751a762f5
--- /dev/null
+++ b/www/icons/ic_acode_default.svg
@@ -0,0 +1,227 @@
+
+
diff --git a/www/icons/ic_acode_midnight_circuit.svg b/www/icons/ic_acode_midnight_circuit.svg
new file mode 100644
index 000000000..772e82baf
--- /dev/null
+++ b/www/icons/ic_acode_midnight_circuit.svg
@@ -0,0 +1,33 @@
+
diff --git a/www/icons/ic_acode_pixel_party.svg b/www/icons/ic_acode_pixel_party.svg
new file mode 100644
index 000000000..40e37b72a
--- /dev/null
+++ b/www/icons/ic_acode_pixel_party.svg
@@ -0,0 +1,29 @@
+
diff --git a/www/icons/ic_acode_solar_flare.svg b/www/icons/ic_acode_solar_flare.svg
new file mode 100644
index 000000000..df2a78319
--- /dev/null
+++ b/www/icons/ic_acode_solar_flare.svg
@@ -0,0 +1,28 @@
+
diff --git a/www/icons/ic_acode_terminal_glow.svg b/www/icons/ic_acode_terminal_glow.svg
new file mode 100644
index 000000000..f5285aba1
--- /dev/null
+++ b/www/icons/ic_acode_terminal_glow.svg
@@ -0,0 +1,33 @@
+