diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index c996f88e6..eb08c54d7 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -29,11 +29,6 @@
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.Nuvio.Splash">
-
-
-
-
-
@@ -74,6 +69,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ option.platformName to "$launcherPackage.${option.platformName ?: "AppIconDefault"}"
+ }
+
+ private var context: Context? = null
+
+ fun initialize(context: Context) {
+ val appContext = context.applicationContext
+ this.context = appContext
+ restoreDefaultIfNeeded(appContext)
+ }
+
+ actual fun currentIconName(): String? {
+ val appContext = context ?: return null
+ return currentIconName(appContext)
+ }
+
+ fun currentLauncherIconResource(context: Context): Int {
+ val option = AppIconOption.fromPlatformName(currentIconName(context))
+ val resourceName = if (option == AppIconOption.ORIGINAL) {
+ "ic_launcher"
+ } else {
+ "ic_launcher_${option.key}"
+ }
+ return context.resources
+ .getIdentifier(resourceName, "mipmap", context.packageName)
+ .takeIf { it != 0 }
+ ?: com.nuvio.app.R.mipmap.ic_launcher
+ }
+
+ fun currentLauncherComponent(context: Context): ComponentName {
+ val currentName = currentIconName(context)
+ val className = launcherComponents.first { it.first == currentName }.second
+ return component(context, className)
+ }
+
+ private fun currentIconName(context: Context): String? {
+ val packageManager = context.packageManager
+ val explicitlyEnabled = launcherComponents.firstOrNull { (_, className) ->
+ packageManager.getComponentEnabledSetting(component(context, className)) ==
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ }
+ return explicitlyEnabled?.first
+ }
+
+ private fun restoreDefaultIfNeeded(context: Context) {
+ val packageManager = context.packageManager
+ val hasEnabledComponent = launcherComponents.any { (_, className) ->
+ packageManager.getComponentEnabledSetting(component(context, className)) ==
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ }
+ if (hasEnabledComponent) return
+
+ val defaultClass = launcherComponents.first { it.first == null }.second
+ if (
+ packageManager.getComponentEnabledSetting(component(context, defaultClass)) !=
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ ) {
+ return
+ }
+ packageManager.setComponentEnabledSetting(
+ component(context, defaultClass),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+ }
+
+ actual suspend fun activateIcon(name: String?): Boolean {
+ val appContext = context ?: return false
+ val selectedClass = launcherComponents.firstOrNull { it.first == name }?.second ?: return false
+ val packageManager = appContext.packageManager
+
+ return runCatching {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.setComponentEnabledSettings(
+ launcherComponents.map { (_, className) ->
+ PackageManager.ComponentEnabledSetting(
+ component(appContext, className),
+ if (className == selectedClass) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ },
+ PackageManager.DONT_KILL_APP,
+ )
+ },
+ )
+ } else {
+ launcherComponents.forEach { (_, className) ->
+ packageManager.setComponentEnabledSetting(
+ component(appContext, className),
+ if (className == selectedClass) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ },
+ PackageManager.DONT_KILL_APP,
+ )
+ }
+ }
+ }.isSuccess
+ }
+
+ private fun component(context: Context, className: String): ComponentName =
+ ComponentName(context.packageName, className)
+}
diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/launcher/AppIconActivities.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/launcher/AppIconActivities.kt
new file mode 100644
index 000000000..8838d222d
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/launcher/AppIconActivities.kt
@@ -0,0 +1,15 @@
+package com.nuvio.app.launcher
+
+import com.nuvio.app.MainActivity
+
+class AppIconDefault : MainActivity()
+
+class AppIconArcticBlue : MainActivity()
+
+class AppIconEmerald : MainActivity()
+
+class AppIconRoseGold : MainActivity()
+
+class AppIconCopper : MainActivity()
+
+class AppIconGraphite : MainActivity()
diff --git a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo.webp b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo.webp
index 7db97d76c..b5230128f 100644
Binary files a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo.webp and b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo.webp differ
diff --git a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_arctic_blue.webp b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_arctic_blue.webp
new file mode 100644
index 000000000..9d47c880f
Binary files /dev/null and b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_arctic_blue.webp differ
diff --git a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_copper.webp b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_copper.webp
new file mode 100644
index 000000000..770e58117
Binary files /dev/null and b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_copper.webp differ
diff --git a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_emerald.webp b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_emerald.webp
new file mode 100644
index 000000000..42b900198
Binary files /dev/null and b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_emerald.webp differ
diff --git a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_graphite.webp b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_graphite.webp
new file mode 100644
index 000000000..2c3fb6d1b
Binary files /dev/null and b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_graphite.webp differ
diff --git a/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_rose_gold.webp b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_rose_gold.webp
new file mode 100644
index 000000000..34cd65693
Binary files /dev/null and b/composeApp/src/androidMain/res/drawable-nodpi/ic_splash_logo_rose_gold.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
index 9d92593cd..425b4f12d 100644
--- a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,6 @@
-
+
-
\ No newline at end of file
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_arctic_blue.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_arctic_blue.xml
new file mode 100644
index 000000000..304c4dfa4
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_arctic_blue.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_copper.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_copper.xml
new file mode 100644
index 000000000..8bb03d676
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_copper.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_emerald.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_emerald.xml
new file mode 100644
index 000000000..ca3919090
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_emerald.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_graphite.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_graphite.xml
new file mode 100644
index 000000000..644caea77
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_graphite.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_rose_gold.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_rose_gold.xml
new file mode 100644
index 000000000..f8cc96e10
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_rose_gold.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 9d92593cd..425b4f12d 100644
--- a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,6 @@
-
+
-
\ No newline at end of file
+
+
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
index fbc06277a..fc0a5c726 100644
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_arctic_blue.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_arctic_blue.webp
new file mode 100644
index 000000000..aea17c429
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_arctic_blue.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_arctic_blue_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_arctic_blue_foreground.webp
new file mode 100644
index 000000000..608805d10
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_arctic_blue_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_copper.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_copper.webp
new file mode 100644
index 000000000..f211a5bc6
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_copper.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_copper_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_copper_foreground.webp
new file mode 100644
index 000000000..a9d6fdc9d
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_copper_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_emerald.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_emerald.webp
new file mode 100644
index 000000000..1330a42c0
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_emerald.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_emerald_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_emerald_foreground.webp
new file mode 100644
index 000000000..2a88efec4
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_emerald_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
index fbc06277a..00a611123 100644
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_graphite.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_graphite.webp
new file mode 100644
index 000000000..423d915c2
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_graphite.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_graphite_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_graphite_foreground.webp
new file mode 100644
index 000000000..7e35ca6d3
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_graphite_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp
new file mode 100644
index 000000000..021f7b562
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_rose_gold.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_rose_gold.webp
new file mode 100644
index 000000000..97ae4d27e
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_rose_gold.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_rose_gold_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_rose_gold_foreground.webp
new file mode 100644
index 000000000..309ab045e
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_rose_gold_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
index fbc06277a..e53a8e1bf 100644
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
index c28a18014..a77ba395a 100644
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_arctic_blue.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_arctic_blue.webp
new file mode 100644
index 000000000..db8b0b702
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_arctic_blue.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_arctic_blue_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_arctic_blue_foreground.webp
new file mode 100644
index 000000000..d0d441a71
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_arctic_blue_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_copper.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_copper.webp
new file mode 100644
index 000000000..3b128b231
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_copper.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_copper_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_copper_foreground.webp
new file mode 100644
index 000000000..07c9d6de8
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_copper_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_emerald.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_emerald.webp
new file mode 100644
index 000000000..974850237
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_emerald.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_emerald_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_emerald_foreground.webp
new file mode 100644
index 000000000..9fb8f1381
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_emerald_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
index c28a18014..92f7966ad 100644
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_graphite.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_graphite.webp
new file mode 100644
index 000000000..e5acaa773
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_graphite.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_graphite_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_graphite_foreground.webp
new file mode 100644
index 000000000..a08d10e07
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_graphite_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp
new file mode 100644
index 000000000..054555dd9
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_rose_gold.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_rose_gold.webp
new file mode 100644
index 000000000..5ddbf07b3
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_rose_gold.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_rose_gold_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_rose_gold_foreground.webp
new file mode 100644
index 000000000..08519ccf9
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_rose_gold_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
index c28a18014..7ff5c43fb 100644
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
index 16206c58c..539a9f04a 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_arctic_blue.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_arctic_blue.webp
new file mode 100644
index 000000000..5b5a40d61
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_arctic_blue.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_arctic_blue_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_arctic_blue_foreground.webp
new file mode 100644
index 000000000..fcf3af857
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_arctic_blue_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_copper.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_copper.webp
new file mode 100644
index 000000000..3d2fbbcfb
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_copper.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_copper_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_copper_foreground.webp
new file mode 100644
index 000000000..b4f692fd4
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_copper_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_emerald.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_emerald.webp
new file mode 100644
index 000000000..a31be3777
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_emerald.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_emerald_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_emerald_foreground.webp
new file mode 100644
index 000000000..718fc2562
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_emerald_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
index 16206c58c..6b7048f5a 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_graphite.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_graphite.webp
new file mode 100644
index 000000000..624839902
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_graphite.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_graphite_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_graphite_foreground.webp
new file mode 100644
index 000000000..a61e11f3f
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_graphite_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp
new file mode 100644
index 000000000..b523662d6
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_rose_gold.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_rose_gold.webp
new file mode 100644
index 000000000..339b01b1b
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_rose_gold.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_rose_gold_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_rose_gold_foreground.webp
new file mode 100644
index 000000000..4ed478b40
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_rose_gold_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
index 16206c58c..c5611b3c1 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
index 7795c137a..db8266226 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_arctic_blue.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_arctic_blue.webp
new file mode 100644
index 000000000..238fb1c63
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_arctic_blue.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_arctic_blue_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_arctic_blue_foreground.webp
new file mode 100644
index 000000000..a9b70c3db
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_arctic_blue_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_copper.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_copper.webp
new file mode 100644
index 000000000..02dc14d65
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_copper.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_copper_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_copper_foreground.webp
new file mode 100644
index 000000000..f3f81d9e5
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_copper_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_emerald.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_emerald.webp
new file mode 100644
index 000000000..2a6d066d7
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_emerald.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_emerald_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_emerald_foreground.webp
new file mode 100644
index 000000000..2761136fa
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_emerald_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
index 7795c137a..e47b08f50 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_graphite.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_graphite.webp
new file mode 100644
index 000000000..797ed1489
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_graphite.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_graphite_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_graphite_foreground.webp
new file mode 100644
index 000000000..41c40e113
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_graphite_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp
new file mode 100644
index 000000000..db9673199
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_rose_gold.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_rose_gold.webp
new file mode 100644
index 000000000..b5f3be314
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_rose_gold.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_rose_gold_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_rose_gold_foreground.webp
new file mode 100644
index 000000000..2872475e7
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_rose_gold_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
index 7795c137a..d902fe55f 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
index 43104830e..403d01a49 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_arctic_blue.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_arctic_blue.webp
new file mode 100644
index 000000000..18ce00f45
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_arctic_blue.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_arctic_blue_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_arctic_blue_foreground.webp
new file mode 100644
index 000000000..c35eae5da
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_arctic_blue_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_copper.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_copper.webp
new file mode 100644
index 000000000..c052bb6a4
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_copper.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_copper_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_copper_foreground.webp
new file mode 100644
index 000000000..f8188adbd
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_copper_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_emerald.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_emerald.webp
new file mode 100644
index 000000000..f65c5990e
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_emerald.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_emerald_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_emerald_foreground.webp
new file mode 100644
index 000000000..3f75b498e
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_emerald_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
index 43104830e..5dac649e6 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_graphite.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_graphite.webp
new file mode 100644
index 000000000..fb558aa5f
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_graphite.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_graphite_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_graphite_foreground.webp
new file mode 100644
index 000000000..2422b037d
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_graphite_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp
new file mode 100644
index 000000000..d9e359628
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_rose_gold.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_rose_gold.webp
new file mode 100644
index 000000000..26dc9fb4e
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_rose_gold.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_rose_gold_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_rose_gold_foreground.webp
new file mode 100644
index 000000000..aa0fb8545
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_rose_gold_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 43104830e..47ec6939e 100644
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/values-v31/themes.xml b/composeApp/src/androidMain/res/values-v31/themes.xml
index 97bab9895..3a996359b 100644
--- a/composeApp/src/androidMain/res/values-v31/themes.xml
+++ b/composeApp/src/androidMain/res/values-v31/themes.xml
@@ -9,4 +9,24 @@
- @drawable/ic_splash_logo
- @style/Theme.Nuvio
+
+
+
+
+
+
+
+
+
+
diff --git a/composeApp/src/androidMain/res/values/colors.xml b/composeApp/src/androidMain/res/values/colors.xml
index da3edf19a..ed834aeea 100644
--- a/composeApp/src/androidMain/res/values/colors.xml
+++ b/composeApp/src/androidMain/res/values/colors.xml
@@ -1,4 +1,5 @@
+ #000000
#0D0D0D
diff --git a/composeApp/src/androidMain/res/values/themes.xml b/composeApp/src/androidMain/res/values/themes.xml
index 91536bd1b..342c5db31 100644
--- a/composeApp/src/androidMain/res/values/themes.xml
+++ b/composeApp/src/androidMain/res/values/themes.xml
@@ -5,4 +5,9 @@
+
+
+
+
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon_arctic_blue.png b/composeApp/src/commonMain/composeResources/drawable/app_icon_arctic_blue.png
new file mode 100644
index 000000000..36fe3a2b0
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_icon_arctic_blue.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon_copper.png b/composeApp/src/commonMain/composeResources/drawable/app_icon_copper.png
new file mode 100644
index 000000000..2a58503b7
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_icon_copper.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon_emerald.png b/composeApp/src/commonMain/composeResources/drawable/app_icon_emerald.png
new file mode 100644
index 000000000..a68be65b7
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_icon_emerald.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon_graphite.png b/composeApp/src/commonMain/composeResources/drawable/app_icon_graphite.png
new file mode 100644
index 000000000..290ad4c16
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_icon_graphite.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon_original.png b/composeApp/src/commonMain/composeResources/drawable/app_icon_original.png
new file mode 100644
index 000000000..791291074
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_icon_original.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon_rose_gold.png b/composeApp/src/commonMain/composeResources/drawable/app_icon_rose_gold.png
new file mode 100644
index 000000000..003ae5c22
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_icon_rose_gold.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_arctic_blue.png b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_arctic_blue.png
new file mode 100644
index 000000000..d02e11c3d
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_arctic_blue.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_copper.png b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_copper.png
new file mode 100644
index 000000000..f61c6e558
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_copper.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_emerald.png b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_emerald.png
new file mode 100644
index 000000000..40d356d3e
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_emerald.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_graphite.png b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_graphite.png
new file mode 100644
index 000000000..13b910e9f
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_graphite.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_original.png b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_original.png
new file mode 100644
index 000000000..5e74d396e
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_original.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_rose_gold.png b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_rose_gold.png
new file mode 100644
index 000000000..44883e596
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/app_logo_wordmark_rose_gold.png differ
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 514177b75..6f54ce85a 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -577,6 +577,17 @@
Signed in
AMOLED Black
Use pure black backgrounds for OLED screens.
+ App Icon
+ Arctic Blue
+ Changing to %1$s will close Nuvio. Tap OK to continue.
+ Change App Icon?
+ The icon could not be changed. Please try again.
+ Copper
+ Emerald
+ Graphite
+ Original
+ Rose Gold
+ Choose App Icon
App Language
Device language
Choose Language
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
index 13a56614e..0a8583013 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
@@ -200,6 +200,7 @@ import com.nuvio.app.features.settings.ContinueWatchingSettingsScreen
import com.nuvio.app.features.settings.AddonsSettingsScreen
import com.nuvio.app.features.settings.PluginsSettingsScreen
import com.nuvio.app.features.settings.AccountSettingsScreen
+import com.nuvio.app.features.settings.AppBrandWordmark
import com.nuvio.app.features.settings.SupportersContributorsSettingsScreen
import com.nuvio.app.features.settings.LicensesAttributionsSettingsScreen
import com.nuvio.app.features.settings.NavBarStyle
@@ -255,7 +256,6 @@ import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import com.nuvio.app.navigation.*
import nuvio.composeapp.generated.resources.*
-import nuvio.composeapp.generated.resources.app_logo_wordmark
import nuvio.composeapp.generated.resources.compose_catalog_subtitle_library
import nuvio.composeapp.generated.resources.compose_catalog_subtitle_trakt_library
import nuvio.composeapp.generated.resources.compose_nav_home
@@ -3936,13 +3936,11 @@ private fun AppLaunchOverlay(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
- Image(
- painter = painterResource(Res.drawable.app_logo_wordmark),
+ AppBrandWordmark(
contentDescription = stringResource(Res.string.app_brand_name),
modifier = Modifier
.fillMaxWidth(0.48f)
.height(44.dp),
- contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(tokens.spacing.sectionGap))
NuvioLoadingIndicator(color = tokens.colors.accent)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt
index b0d3167cf..93bb4d837 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt
@@ -5,7 +5,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -66,7 +65,6 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
@@ -84,13 +82,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.auth.AuthRepository
+import com.nuvio.app.features.settings.AppBrandWordmark
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.PI
import kotlin.math.sin
import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.Res
-import nuvio.composeapp.generated.resources.app_logo_wordmark
import nuvio.composeapp.generated.resources.compose_auth_already_have_account
import nuvio.composeapp.generated.resources.compose_auth_continue_without_account
import nuvio.composeapp.generated.resources.compose_auth_create_account
@@ -107,7 +105,6 @@ import nuvio.composeapp.generated.resources.compose_auth_tagline
import nuvio.composeapp.generated.resources.compose_auth_terms_link
import nuvio.composeapp.generated.resources.compose_auth_terms_prefix
import nuvio.composeapp.generated.resources.compose_auth_welcome_back
-import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
private val AuthTextPrimary = Color(0xFFF5F7F8)
@@ -429,11 +426,9 @@ private fun AuthLargeLayout(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
) {
- Image(
- painter = painterResource(Res.drawable.app_logo_wordmark),
+ AppBrandWordmark(
contentDescription = null,
modifier = Modifier.height(60.dp * scale),
- contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(32.dp * scale))
Text(
@@ -527,11 +522,9 @@ private fun AuthBrandLockup(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
- Image(
- painter = painterResource(Res.drawable.app_logo_wordmark),
+ AppBrandWordmark(
contentDescription = null,
modifier = Modifier.height(logoHeight),
- contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(14.dp))
Text(
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppBrandWordmark.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppBrandWordmark.kt
new file mode 100644
index 000000000..da2f17dcf
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppBrandWordmark.kt
@@ -0,0 +1,28 @@
+package com.nuvio.app.features.settings
+
+import androidx.compose.foundation.Image
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.jetbrains.compose.resources.painterResource
+
+@Composable
+internal fun AppBrandWordmark(
+ modifier: Modifier = Modifier,
+ contentDescription: String? = null,
+ icon: AppIconOption? = null,
+) {
+ val state by remember {
+ AppIconRepository.ensureLoaded()
+ AppIconRepository.state
+ }.collectAsStateWithLifecycle()
+ Image(
+ painter = painterResource((icon ?: state.selected).wordmarkResource),
+ contentDescription = contentDescription,
+ modifier = modifier,
+ contentScale = ContentScale.Fit,
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconOption.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconOption.kt
new file mode 100644
index 000000000..2a06b6095
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconOption.kt
@@ -0,0 +1,37 @@
+package com.nuvio.app.features.settings
+
+internal enum class AppIconOption(
+ val key: String,
+ val platformName: String?,
+) {
+ ORIGINAL(
+ key = "original",
+ platformName = null,
+ ),
+ ARCTIC_BLUE(
+ key = "arctic_blue",
+ platformName = "AppIconArcticBlue",
+ ),
+ EMERALD(
+ key = "emerald",
+ platformName = "AppIconEmerald",
+ ),
+ ROSE_GOLD(
+ key = "rose_gold",
+ platformName = "AppIconRoseGold",
+ ),
+ COPPER(
+ key = "copper",
+ platformName = "AppIconCopper",
+ ),
+ GRAPHITE(
+ key = "graphite",
+ platformName = "AppIconGraphite",
+ ),
+ ;
+
+ companion object {
+ fun fromPlatformName(name: String?): AppIconOption =
+ entries.firstOrNull { it.platformName == name } ?: ORIGINAL
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconPicker.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconPicker.kt
new file mode 100644
index 000000000..60fa85b27
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconPicker.kt
@@ -0,0 +1,330 @@
+package com.nuvio.app.features.settings
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Check
+import androidx.compose.material3.BasicAlertDialog
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.nuvio.app.core.ui.NuvioModalBottomSheet
+import com.nuvio.app.core.ui.NuvioStatusModal
+import com.nuvio.app.core.ui.dismissNuvioBottomSheet
+import com.nuvio.app.core.ui.nuvio
+import kotlinx.coroutines.launch
+import nuvio.composeapp.generated.resources.Res
+import nuvio.composeapp.generated.resources.action_cancel
+import nuvio.composeapp.generated.resources.action_ok
+import nuvio.composeapp.generated.resources.settings_appearance_app_icon_android_confirmation_message
+import nuvio.composeapp.generated.resources.settings_appearance_app_icon_android_confirmation_title
+import nuvio.composeapp.generated.resources.settings_appearance_app_icon_change_failed
+import nuvio.composeapp.generated.resources.settings_appearance_app_icon_sheet_title
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+internal fun AppIconPicker(
+ isTablet: Boolean,
+ state: AppIconSettingsState,
+ onSelected: (AppIconOption) -> Unit,
+ onDismiss: () -> Unit,
+) {
+ var confirmationIcon by remember { mutableStateOf(null) }
+ val requestSelection: (AppIconOption) -> Unit = { icon ->
+ if (icon != state.selected) {
+ if (AppIconPlatform.requiresCloseConfirmation) {
+ confirmationIcon = icon
+ } else {
+ onSelected(icon)
+ }
+ }
+ }
+
+ if (isTablet) {
+ AppIconPickerDialog(
+ state = state,
+ onSelected = requestSelection,
+ onDismiss = onDismiss,
+ )
+ } else {
+ AppIconPickerBottomSheet(
+ state = state,
+ onSelected = requestSelection,
+ onDismiss = onDismiss,
+ )
+ }
+
+ confirmationIcon?.let { icon ->
+ NuvioStatusModal(
+ title = stringResource(Res.string.settings_appearance_app_icon_android_confirmation_title),
+ message = stringResource(
+ Res.string.settings_appearance_app_icon_android_confirmation_message,
+ stringResource(icon.labelResource),
+ ),
+ isVisible = true,
+ confirmText = stringResource(Res.string.action_ok),
+ dismissText = stringResource(Res.string.action_cancel),
+ onConfirm = {
+ confirmationIcon = null
+ onSelected(icon)
+ },
+ onDismiss = { confirmationIcon = null },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AppIconPickerBottomSheet(
+ state: AppIconSettingsState,
+ onSelected: (AppIconOption) -> Unit,
+ onDismiss: () -> Unit,
+) {
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ val scope = rememberCoroutineScope()
+
+ fun dismiss() {
+ scope.launch {
+ dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss)
+ }
+ }
+
+ NuvioModalBottomSheet(
+ onDismissRequest = ::dismiss,
+ sheetState = sheetState,
+ ) {
+ AppIconPickerContent(
+ state = state,
+ columns = 2,
+ onSelected = onSelected,
+ modifier = Modifier.navigationBarsPadding(),
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AppIconPickerDialog(
+ state: AppIconSettingsState,
+ onSelected: (AppIconOption) -> Unit,
+ onDismiss: () -> Unit,
+) {
+ val tokens = MaterialTheme.nuvio
+ BasicAlertDialog(onDismissRequest = onDismiss) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .widthIn(max = tokens.components.dialogMaxWidth),
+ shape = tokens.shapes.dialog,
+ color = tokens.colors.surfaceDialog,
+ ) {
+ AppIconPickerContent(
+ state = state,
+ columns = 3,
+ onSelected = onSelected,
+ )
+ }
+ }
+}
+
+@Composable
+private fun AppIconPickerContent(
+ state: AppIconSettingsState,
+ columns: Int,
+ onSelected: (AppIconOption) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val tokens = MaterialTheme.nuvio
+ val horizontalPadding = if (columns == 4) tokens.spacing.dialogPadding else tokens.spacing.screenHorizontal
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .heightIn(max = 720.dp)
+ .verticalScroll(rememberScrollState())
+ .padding(start = horizontalPadding, top = 10.dp, end = horizontalPadding, bottom = 20.dp),
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_appearance_app_icon_sheet_title),
+ style = MaterialTheme.typography.headlineSmall,
+ color = tokens.colors.textPrimary,
+ fontWeight = FontWeight.SemiBold,
+ )
+
+ if (state.changeFailed) {
+ Spacer(modifier = Modifier.height(14.dp))
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = tokens.shapes.compactCard,
+ color = tokens.colors.danger.copy(alpha = 0.12f),
+ ) {
+ Text(
+ text = stringResource(Res.string.settings_appearance_app_icon_change_failed),
+ modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
+ style = MaterialTheme.typography.bodyMedium,
+ color = tokens.colors.danger,
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(18.dp))
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ AppIconOption.entries.chunked(columns).forEach { rowIcons ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ rowIcons.forEach { icon ->
+ AppIconChoice(
+ icon = icon,
+ selected = state.selected == icon,
+ pending = state.pending == icon,
+ enabled = state.pending == null,
+ onClick = { onSelected(icon) },
+ modifier = Modifier.weight(1f),
+ )
+ }
+ repeat(columns - rowIcons.size) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AppIconChoice(
+ icon: AppIconOption,
+ selected: Boolean,
+ pending: Boolean,
+ enabled: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val tokens = MaterialTheme.nuvio
+ val label = stringResource(icon.labelResource)
+ Surface(
+ onClick = onClick,
+ modifier = modifier.semantics { this.selected = selected },
+ enabled = enabled,
+ shape = tokens.shapes.compactCard,
+ color = if (selected) tokens.colors.overlaySelected else tokens.colors.surfaceCard,
+ border = BorderStroke(
+ width = if (selected) tokens.borders.medium else tokens.borders.hairline,
+ color = if (selected) tokens.colors.borderSelected else tokens.colors.borderSubtle,
+ ),
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box {
+ AppIconThumbnail(
+ icon = icon,
+ modifier = Modifier.size(78.dp),
+ )
+ if (selected || pending) {
+ Surface(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(5.dp)
+ .size(24.dp),
+ shape = CircleShape,
+ color = tokens.colors.accent,
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ if (pending) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(14.dp),
+ color = tokens.colors.onAccent,
+ strokeWidth = 2.dp,
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Rounded.Check,
+ contentDescription = null,
+ modifier = Modifier.size(15.dp),
+ tint = tokens.colors.onAccent,
+ )
+ }
+ }
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(9.dp))
+ Text(
+ text = label,
+ minLines = 2,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.labelLarge,
+ color = if (selected) tokens.colors.textPrimary else tokens.colors.textSecondary,
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
+ )
+ }
+ }
+}
+
+@Composable
+internal fun AppIconThumbnail(
+ icon: AppIconOption,
+ modifier: Modifier = Modifier,
+ contentDescription: String? = null,
+ cornerRadius: Dp = 18.dp,
+) {
+ val tokens = MaterialTheme.nuvio
+ Image(
+ painter = painterResource(icon.previewResource),
+ contentDescription = contentDescription,
+ modifier = modifier
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(cornerRadius))
+ .border(
+ width = tokens.borders.hairline,
+ color = tokens.colors.borderSubtle,
+ shape = RoundedCornerShape(cornerRadius),
+ ),
+ contentScale = ContentScale.Fit,
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconPlatform.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconPlatform.kt
new file mode 100644
index 000000000..97c66cf43
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconPlatform.kt
@@ -0,0 +1,7 @@
+package com.nuvio.app.features.settings
+
+internal expect object AppIconPlatform {
+ val requiresCloseConfirmation: Boolean
+ fun currentIconName(): String?
+ suspend fun activateIcon(name: String?): Boolean
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconRepository.kt
new file mode 100644
index 000000000..a9f69a045
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconRepository.kt
@@ -0,0 +1,52 @@
+package com.nuvio.app.features.settings
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+internal data class AppIconSettingsState(
+ val selected: AppIconOption = AppIconOption.ORIGINAL,
+ val pending: AppIconOption? = null,
+ val changeFailed: Boolean = false,
+)
+
+internal object AppIconRepository {
+ private val _state = MutableStateFlow(AppIconSettingsState())
+ val state: StateFlow = _state.asStateFlow()
+
+ private var hasLoaded = false
+
+ fun ensureLoaded() {
+ if (hasLoaded) return
+ hasLoaded = true
+ _state.value = AppIconSettingsState(
+ selected = AppIconOption.fromPlatformName(AppIconPlatform.currentIconName()),
+ )
+ }
+
+ suspend fun select(icon: AppIconOption) {
+ ensureLoaded()
+ val current = _state.value
+ if (current.pending != null || current.selected == icon) return
+
+ _state.value = current.copy(
+ pending = icon,
+ changeFailed = false,
+ )
+
+ val changed = runCatching {
+ AppIconPlatform.activateIcon(icon.platformName)
+ }.getOrDefault(false)
+ _state.value = if (changed) {
+ AppIconSettingsState(selected = icon)
+ } else {
+ current.copy(changeFailed = true)
+ }
+ }
+
+ fun clearFailure() {
+ val current = _state.value
+ if (!current.changeFailed) return
+ _state.value = current.copy(changeFailed = false)
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconResources.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconResources.kt
new file mode 100644
index 000000000..380cce562
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppIconResources.kt
@@ -0,0 +1,35 @@
+package com.nuvio.app.features.settings
+
+import nuvio.composeapp.generated.resources.*
+import org.jetbrains.compose.resources.DrawableResource
+import org.jetbrains.compose.resources.StringResource
+
+internal val AppIconOption.labelResource: StringResource
+ get() = when (this) {
+ AppIconOption.ORIGINAL -> Res.string.settings_appearance_app_icon_original
+ AppIconOption.ARCTIC_BLUE -> Res.string.settings_appearance_app_icon_arctic_blue
+ AppIconOption.EMERALD -> Res.string.settings_appearance_app_icon_emerald
+ AppIconOption.ROSE_GOLD -> Res.string.settings_appearance_app_icon_rose_gold
+ AppIconOption.COPPER -> Res.string.settings_appearance_app_icon_copper
+ AppIconOption.GRAPHITE -> Res.string.settings_appearance_app_icon_graphite
+ }
+
+internal val AppIconOption.previewResource: DrawableResource
+ get() = when (this) {
+ AppIconOption.ORIGINAL -> Res.drawable.app_icon_original
+ AppIconOption.ARCTIC_BLUE -> Res.drawable.app_icon_arctic_blue
+ AppIconOption.EMERALD -> Res.drawable.app_icon_emerald
+ AppIconOption.ROSE_GOLD -> Res.drawable.app_icon_rose_gold
+ AppIconOption.COPPER -> Res.drawable.app_icon_copper
+ AppIconOption.GRAPHITE -> Res.drawable.app_icon_graphite
+ }
+
+internal val AppIconOption.wordmarkResource: DrawableResource
+ get() = when (this) {
+ AppIconOption.ORIGINAL -> Res.drawable.app_logo_wordmark_original
+ AppIconOption.ARCTIC_BLUE -> Res.drawable.app_logo_wordmark_arctic_blue
+ AppIconOption.EMERALD -> Res.drawable.app_logo_wordmark_emerald
+ AppIconOption.ROSE_GOLD -> Res.drawable.app_logo_wordmark_rose_gold
+ AppIconOption.COPPER -> Res.drawable.app_logo_wordmark_copper
+ AppIconOption.GRAPHITE -> Res.drawable.app_logo_wordmark_graphite
+ }
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt
index f33af7536..ccd36deaa 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt
@@ -56,6 +56,7 @@ import nuvio.composeapp.generated.resources.compose_settings_page_poster_customi
import nuvio.composeapp.generated.resources.compose_settings_page_streams
import nuvio.composeapp.generated.resources.settings_appearance_app_language
import nuvio.composeapp.generated.resources.settings_appearance_app_language_sheet_title
+import nuvio.composeapp.generated.resources.settings_appearance_app_icon
import nuvio.composeapp.generated.resources.settings_appearance_nav_bar_style
import nuvio.composeapp.generated.resources.settings_appearance_nav_bar_style_sheet_title
import nuvio.composeapp.generated.resources.settings_appearance_amoled_black
@@ -87,6 +88,9 @@ internal fun LazyListScope.appearanceSettingsContent(
liquidGlassNativeTabBarSupported: Boolean,
liquidGlassNativeTabBarEnabled: Boolean,
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
+ appIconState: AppIconSettingsState,
+ onAppIconSelected: (AppIconOption) -> Unit,
+ onAppIconFailureDismissed: () -> Unit,
selectedAppLanguage: AppLanguage,
onAppLanguageSelected: (AppLanguage) -> Unit,
selectedNavBarStyle: NavBarStyle,
@@ -152,6 +156,7 @@ internal fun LazyListScope.appearanceSettingsContent(
item {
var showLanguageSheet by remember { mutableStateOf(false) }
var showNavBarStyleSheet by remember { mutableStateOf(false) }
+ var showAppIconPicker by remember { mutableStateOf(false) }
SettingsSection(
title = stringResource(Res.string.settings_appearance_section_display),
isTablet = isTablet,
@@ -175,6 +180,24 @@ internal fun LazyListScope.appearanceSettingsContent(
)
}
SettingsGroupDivider(isTablet = isTablet)
+ SettingsNavigationRow(
+ title = stringResource(Res.string.settings_appearance_app_icon),
+ description = stringResource(appIconState.selected.labelResource),
+ enabled = appIconState.pending == null,
+ isTablet = isTablet,
+ trailingContent = {
+ AppIconThumbnail(
+ icon = appIconState.selected,
+ modifier = Modifier.size(if (isTablet) 44.dp else 40.dp),
+ cornerRadius = if (isTablet) 11.dp else 10.dp,
+ )
+ },
+ onClick = {
+ onAppIconFailureDismissed()
+ showAppIconPicker = true
+ },
+ )
+ SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = stringResource(Res.string.settings_appearance_app_language),
description = stringResource(selectedAppLanguage.labelRes),
@@ -204,6 +227,18 @@ internal fun LazyListScope.appearanceSettingsContent(
)
}
+ if (showAppIconPicker) {
+ AppIconPicker(
+ isTablet = isTablet,
+ state = appIconState,
+ onSelected = onAppIconSelected,
+ onDismiss = {
+ onAppIconFailureDismissed()
+ showAppIconPicker = false
+ },
+ )
+ }
+
if (showNavBarStyleSheet) {
NavBarStyleBottomSheet(
selectedStyle = selectedNavBarStyle,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
index 7d3bc8d50..8c9032713 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
@@ -158,6 +158,14 @@ fun SettingsScreen(
}
val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle()
val navBarStyle by remember { ThemeSettingsRepository.navBarStyle }.collectAsStateWithLifecycle()
+ val appIconState by remember {
+ AppIconRepository.ensureLoaded()
+ AppIconRepository.state
+ }.collectAsStateWithLifecycle()
+ val appIconScope = rememberCoroutineScope()
+ val onAppIconSelected: (AppIconOption) -> Unit = { icon ->
+ appIconScope.launch { AppIconRepository.select(icon) }
+ }
val tmdbSettings by remember {
TmdbSettingsRepository.ensureLoaded()
TmdbSettingsRepository.uiState
@@ -397,6 +405,9 @@ fun SettingsScreen(
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
+ appIconState = appIconState,
+ onAppIconSelected = onAppIconSelected,
+ onAppIconFailureDismissed = AppIconRepository::clearFailure,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
navBarStyle = navBarStyle,
@@ -458,6 +469,9 @@ fun SettingsScreen(
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
+ appIconState = appIconState,
+ onAppIconSelected = onAppIconSelected,
+ onAppIconFailureDismissed = AppIconRepository::clearFailure,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
navBarStyle = navBarStyle,
@@ -529,6 +543,9 @@ private fun MobileSettingsScreen(
liquidGlassNativeTabBarSupported: Boolean,
liquidGlassNativeTabBarEnabled: Boolean,
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
+ appIconState: AppIconSettingsState,
+ onAppIconSelected: (AppIconOption) -> Unit,
+ onAppIconFailureDismissed: () -> Unit,
selectedAppLanguage: AppLanguage,
onAppLanguageSelected: (AppLanguage) -> Unit,
navBarStyle: NavBarStyle,
@@ -727,6 +744,9 @@ private fun MobileSettingsScreen(
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
onLiquidGlassNativeTabBarToggle = onLiquidGlassNativeTabBarToggle,
+ appIconState = appIconState,
+ onAppIconSelected = onAppIconSelected,
+ onAppIconFailureDismissed = onAppIconFailureDismissed,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = onAppLanguageSelected,
selectedNavBarStyle = navBarStyle,
@@ -887,6 +907,9 @@ private fun TabletSettingsScreen(
liquidGlassNativeTabBarSupported: Boolean,
liquidGlassNativeTabBarEnabled: Boolean,
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
+ appIconState: AppIconSettingsState,
+ onAppIconSelected: (AppIconOption) -> Unit,
+ onAppIconFailureDismissed: () -> Unit,
selectedAppLanguage: AppLanguage,
onAppLanguageSelected: (AppLanguage) -> Unit,
navBarStyle: NavBarStyle,
@@ -1141,6 +1164,9 @@ private fun TabletSettingsScreen(
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
onLiquidGlassNativeTabBarToggle = onLiquidGlassNativeTabBarToggle,
+ appIconState = appIconState,
+ onAppIconSelected = onAppIconSelected,
+ onAppIconFailureDismissed = onAppIconFailureDismissed,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = onAppLanguageSelected,
selectedNavBarStyle = navBarStyle,
diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/settings/AppIconOptionTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/settings/AppIconOptionTest.kt
new file mode 100644
index 000000000..e7ce5b60d
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/settings/AppIconOptionTest.kt
@@ -0,0 +1,29 @@
+package com.nuvio.app.features.settings
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class AppIconOptionTest {
+ @Test
+ fun primaryIconUsesPlatformDefault() {
+ assertEquals(null, AppIconOption.ORIGINAL.platformName)
+ assertEquals(AppIconOption.ORIGINAL, AppIconOption.fromPlatformName(null))
+ }
+
+ @Test
+ fun alternateIconNamesRoundTrip() {
+ AppIconOption.entries.drop(1).forEach { icon ->
+ assertEquals(icon, AppIconOption.fromPlatformName(icon.platformName))
+ }
+ }
+
+ @Test
+ fun shortlistedCatalogueContainsSixIcons() {
+ assertEquals(6, AppIconOption.entries.size)
+ }
+
+ @Test
+ fun unknownIconFallsBackToOriginal() {
+ assertEquals(AppIconOption.ORIGINAL, AppIconOption.fromPlatformName("UnknownIcon"))
+ }
+}
diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/AppIconPlatform.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/AppIconPlatform.ios.kt
new file mode 100644
index 000000000..0ec6cac14
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/AppIconPlatform.ios.kt
@@ -0,0 +1,55 @@
+package com.nuvio.app.features.settings
+
+import com.nuvio.app.nativebridge.NuvioIsCurrentAlternateAppIcon
+import com.nuvio.app.nativebridge.NuvioSetAlternateAppIconName
+import com.nuvio.app.nativebridge.NuvioSupportsAlternateAppIcons
+import kotlinx.cinterop.CFunction
+import kotlinx.cinterop.CPointer
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.staticCFunction
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+
+@OptIn(ExperimentalForeignApi::class)
+internal actual object AppIconPlatform {
+ actual val requiresCloseConfirmation: Boolean = false
+
+ private var pendingChange: CancellableContinuation? = null
+
+ actual fun currentIconName(): String? = AppIconOption.entries
+ .mapNotNull(AppIconOption::platformName)
+ .firstOrNull { name -> NuvioIsCurrentAlternateAppIcon(name) }
+
+ actual suspend fun activateIcon(name: String?): Boolean {
+ if (!NuvioSupportsAlternateAppIcons()) return false
+
+ return suspendCancellableCoroutine { continuation ->
+ if (pendingChange != null) {
+ continuation.resume(false)
+ return@suspendCancellableCoroutine
+ }
+
+ pendingChange = continuation
+ continuation.invokeOnCancellation {
+ if (pendingChange === continuation) {
+ pendingChange = null
+ }
+ }
+
+ NuvioSetAlternateAppIconName(name, appIconCompletion)
+ }
+ }
+
+ fun completeChange(success: Boolean) {
+ val continuation = pendingChange ?: return
+ pendingChange = null
+ if (continuation.isActive) {
+ continuation.resume(success)
+ }
+ }
+}
+
+@OptIn(ExperimentalForeignApi::class)
+private val appIconCompletion: CPointer Unit>> =
+ staticCFunction { success -> AppIconPlatform.completeChange(success) }
diff --git a/composeApp/src/nativeInterop/cinterop/AppIconBridge.h b/composeApp/src/nativeInterop/cinterop/AppIconBridge.h
new file mode 100644
index 000000000..01b4fadcd
--- /dev/null
+++ b/composeApp/src/nativeInterop/cinterop/AppIconBridge.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include
+
+typedef void (*NuvioAppIconCompletion)(bool);
+
+bool NuvioSupportsAlternateAppIcons(void);
+bool NuvioIsCurrentAlternateAppIcon(const char *name);
+void NuvioSetAlternateAppIconName(const char *name, NuvioAppIconCompletion completion);
diff --git a/composeApp/src/nativeInterop/cinterop/appicon.def b/composeApp/src/nativeInterop/cinterop/appicon.def
new file mode 100644
index 000000000..1fbc34b17
--- /dev/null
+++ b/composeApp/src/nativeInterop/cinterop/appicon.def
@@ -0,0 +1,2 @@
+headers = AppIconBridge.h
+package = com.nuvio.app.nativebridge
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index f15b3beb2..39472a2da 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -386,8 +386,16 @@
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = (
+ AppIconArcticBlue,
+ AppIconEmerald,
+ AppIconRoseGold,
+ AppIconCopper,
+ AppIconGraphite,
+ );
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
@@ -421,8 +429,16 @@
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = (
+ AppIconArcticBlue,
+ AppIconEmerald,
+ AppIconRoseGold,
+ AppIconCopper,
+ AppIconGraphite,
+ );
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
diff --git a/iosApp/iosApp/AppIconBridge.m b/iosApp/iosApp/AppIconBridge.m
new file mode 100644
index 000000000..675edd8d7
--- /dev/null
+++ b/iosApp/iosApp/AppIconBridge.m
@@ -0,0 +1,41 @@
+#import
+#include
+
+typedef void (*NuvioAppIconCompletion)(bool);
+
+bool NuvioSupportsAlternateAppIcons(void) {
+ return UIApplication.sharedApplication.supportsAlternateIcons;
+}
+
+bool NuvioIsCurrentAlternateAppIcon(const char *name) {
+ NSString *currentName = UIApplication.sharedApplication.alternateIconName;
+ if (name == NULL) {
+ return currentName == nil;
+ }
+ return [currentName isEqualToString:[NSString stringWithUTF8String:name]];
+}
+
+void NuvioSetAlternateAppIconName(const char *name, NuvioAppIconCompletion completion) {
+ NSString *iconName = name == NULL ? nil : [NSString stringWithUTF8String:name];
+ void (^changeIcon)(void) = ^{
+ UIApplication *application = UIApplication.sharedApplication;
+ if (!application.supportsAlternateIcons) {
+ if (completion != NULL) {
+ completion(false);
+ }
+ return;
+ }
+ [application setAlternateIconName:iconName completionHandler:^(NSError *error) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (completion != NULL) {
+ completion(error == nil);
+ }
+ });
+ }];
+ };
+ if (NSThread.isMainThread) {
+ changeIcon();
+ } else {
+ dispatch_async(dispatch_get_main_queue(), changeIcon);
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
index 4e8d485bf..fbe7ce193 100644
--- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,36 +1,36 @@
{
- "images" : [
+ "images": [
{
- "filename" : "app-icon-1024.png",
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
},
{
- "appearances" : [
+ "appearances": [
{
- "appearance" : "luminosity",
- "value" : "dark"
+ "appearance": "luminosity",
+ "value": "dark"
}
],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
},
{
- "appearances" : [
+ "appearances": [
{
- "appearance" : "luminosity",
- "value" : "tinted"
+ "appearance": "luminosity",
+ "value": "tinted"
}
],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
}
],
- "info" : {
- "author" : "xcode",
- "version" : 1
+ "info": {
+ "author": "xcode",
+ "version": 1
}
}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
index 516ca8356..71d8b1eaa 100644
Binary files a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconArcticBlue.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIconArcticBlue.appiconset/Contents.json
new file mode 100644
index 000000000..fbe7ce193
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIconArcticBlue.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images": [
+ {
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconArcticBlue.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIconArcticBlue.appiconset/app-icon-1024.png
new file mode 100644
index 000000000..8ad395392
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIconArcticBlue.appiconset/app-icon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconCopper.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIconCopper.appiconset/Contents.json
new file mode 100644
index 000000000..fbe7ce193
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIconCopper.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images": [
+ {
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconCopper.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIconCopper.appiconset/app-icon-1024.png
new file mode 100644
index 000000000..312bcd84e
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIconCopper.appiconset/app-icon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconEmerald.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIconEmerald.appiconset/Contents.json
new file mode 100644
index 000000000..fbe7ce193
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIconEmerald.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images": [
+ {
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconEmerald.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIconEmerald.appiconset/app-icon-1024.png
new file mode 100644
index 000000000..e02219f73
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIconEmerald.appiconset/app-icon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconGraphite.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIconGraphite.appiconset/Contents.json
new file mode 100644
index 000000000..fbe7ce193
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIconGraphite.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images": [
+ {
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconGraphite.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIconGraphite.appiconset/app-icon-1024.png
new file mode 100644
index 000000000..533869781
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIconGraphite.appiconset/app-icon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconRoseGold.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIconRoseGold.appiconset/Contents.json
new file mode 100644
index 000000000..fbe7ce193
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIconRoseGold.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images": [
+ {
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/AppIconRoseGold.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIconRoseGold.appiconset/app-icon-1024.png
new file mode 100644
index 000000000..400d0dc4e
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIconRoseGold.appiconset/app-icon-1024.png differ
diff --git a/scripts/generate_app_icon_assets.py b/scripts/generate_app_icon_assets.py
new file mode 100644
index 000000000..ae70b1ff5
--- /dev/null
+++ b/scripts/generate_app_icon_assets.py
@@ -0,0 +1,358 @@
+from pathlib import Path
+import json
+import shutil
+
+from PIL import Image, ImageDraw, ImageFont
+
+
+ROOT = Path(__file__).resolve().parents[1]
+COLORWAYS = ROOT / "asset/Logo Colorways - Black"
+ANDROID_RES = ROOT / "composeApp/src/androidMain/res"
+ANDROID_THEMES = ANDROID_RES / "values/themes.xml"
+ANDROID_THEMES_V31 = ANDROID_RES / "values-v31/themes.xml"
+ANDROID_SPLASH = ANDROID_RES / "drawable-nodpi"
+COMPOSE_DRAWABLE = ROOT / "composeApp/src/commonMain/composeResources/drawable"
+IOS_ASSETS = ROOT / "iosApp/iosApp/Assets.xcassets"
+PREVIEW_BOARD = ROOT / "build/app-icon-full-catalogue.png"
+WORDMARK_SOURCE = COMPOSE_DRAWABLE / "app_logo_wordmark.png"
+FONT = Path("/System/Library/Fonts/SFNS.ttf")
+
+ALL_VARIANTS = [
+ ("original", "01-original-on-black.png", "AppIcon", "Original"),
+ ("glacier", "02-glacier.png", "AppIconGlacier", "Glacier"),
+ ("arctic_blue", "03-arctic-blue.png", "AppIconArcticBlue", "Arctic Blue"),
+ ("cobalt_pulse", "04-cobalt-pulse.png", "AppIconCobaltPulse", "Cobalt Pulse"),
+ ("sapphire", "05-sapphire.png", "AppIconSapphire", "Sapphire"),
+ ("electric_violet", "06-electric-violet.png", "AppIconElectricViolet", "Electric Violet"),
+ ("polar_mint", "07-polar-mint.png", "AppIconPolarMint", "Polar Mint"),
+ ("tidal", "08-tidal.png", "AppIconTidal", "Tidal"),
+ ("turquoise", "09-turquoise.png", "AppIconTurquoise", "Turquoise"),
+ ("emerald", "10-emerald.png", "AppIconEmerald", "Emerald"),
+ ("jade", "11-jade.png", "AppIconJade", "Jade"),
+ ("lime_pulse", "12-lime-pulse.png", "AppIconLimePulse", "Lime Pulse"),
+ ("sunset", "13-sunset.png", "AppIconSunset", "Sunset"),
+ ("magenta_rush", "14-magenta-rush.png", "AppIconMagentaRush", "Magenta Rush"),
+ ("cherry_pop", "15-cherry-pop.png", "AppIconCherryPop", "Cherry Pop"),
+ ("crimson", "16-crimson.png", "AppIconCrimson", "Crimson"),
+ ("ember_gold", "17-ember-gold.png", "AppIconEmberGold", "Ember Gold"),
+ ("solar", "18-solar.png", "AppIconSolar", "Solar"),
+ ("rose_gold", "19-rose-gold.png", "AppIconRoseGold", "Rose Gold"),
+ ("amethyst", "20-amethyst.png", "AppIconAmethyst", "Amethyst"),
+ ("burgundy", "21-burgundy.png", "AppIconBurgundy", "Burgundy"),
+ ("copper", "22-copper.png", "AppIconCopper", "Copper"),
+ ("champagne", "23-champagne.png", "AppIconChampagne", "Champagne"),
+ ("graphite", "24-graphite.png", "AppIconGraphite", "Graphite"),
+]
+
+SHORTLIST_KEYS = {
+ "original",
+ "arctic_blue",
+ "emerald",
+ "rose_gold",
+ "copper",
+ "graphite",
+}
+
+VARIANTS = [variant for variant in ALL_VARIANTS if variant[0] in SHORTLIST_KEYS]
+
+DENSITIES = {
+ "mdpi": 1.0,
+ "hdpi": 1.5,
+ "xhdpi": 2.0,
+ "xxhdpi": 3.0,
+ "xxxhdpi": 4.0,
+}
+
+
+def resized(image: Image.Image, size: int) -> Image.Image:
+ return image.resize((size, size), Image.Resampling.LANCZOS)
+
+
+def expanded_logo(
+ image: Image.Image,
+ max_width_ratio: float,
+ max_height_ratio: float,
+) -> Image.Image:
+ rgba = image.convert("RGBA")
+ bounds = rgba.getchannel("A").getbbox()
+ if bounds is None:
+ return rgba
+ mark = rgba.crop(bounds)
+ scale = min(
+ rgba.width * max_width_ratio / mark.width,
+ rgba.height * max_height_ratio / mark.height,
+ )
+ mark = mark.resize(
+ (round(mark.width * scale), round(mark.height * scale)),
+ Image.Resampling.LANCZOS,
+ )
+ original_center_x = ((bounds[0] + bounds[2]) / 2) / rgba.width
+ original_center_y = ((bounds[1] + bounds[3]) / 2) / rgba.height
+ x = round(rgba.width * original_center_x - mark.width / 2)
+ y = round(rgba.height * original_center_y - mark.height / 2)
+ canvas = Image.new("RGBA", rgba.size, (0, 0, 0, 0))
+ canvas.alpha_composite(mark, (x, y))
+ return canvas
+
+
+def on_black(image: Image.Image) -> Image.Image:
+ canvas = Image.new("RGBA", image.size, (0, 0, 0, 255))
+ canvas.alpha_composite(image)
+ return canvas.convert("RGB")
+
+
+def wordmark_for(image: Image.Image) -> Image.Image:
+ base = Image.open(WORDMARK_SOURCE).convert("RGBA")
+ bounds = image.getchannel("A").getbbox()
+ if bounds is None:
+ return base
+ mark = image.crop(bounds)
+ scale = base.height / mark.height
+ mark = mark.resize(
+ (round(mark.width * scale), base.height),
+ Image.Resampling.LANCZOS,
+ )
+ canvas = base.copy()
+ canvas.paste((0, 0, 0, 0), (0, 0, round(base.height * 1.04), base.height))
+ canvas.alpha_composite(mark, (0, 0))
+ return canvas
+
+
+def save_webp(image: Image.Image, path: Path) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ image.save(path, "WEBP", lossless=True, method=6)
+
+
+def round_icon(image: Image.Image) -> Image.Image:
+ rgba = image.convert("RGBA")
+ mask = Image.new("L", rgba.size, 0)
+ ImageDraw.Draw(mask).ellipse((0, 0, rgba.width - 1, rgba.height - 1), fill=255)
+ rgba.putalpha(mask)
+ return rgba
+
+
+def monochrome_icon(image: Image.Image) -> Image.Image:
+ rgba = image.convert("RGBA")
+ white = Image.new("RGBA", rgba.size, (255, 255, 255, 0))
+ white.putalpha(rgba.getchannel("A"))
+ return white
+
+
+def ios_contents() -> dict:
+ return {
+ "images": [
+ {
+ "filename": "app-icon-1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024",
+ },
+ {
+ "appearances": [{"appearance": "luminosity", "value": "dark"}],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024",
+ },
+ {
+ "appearances": [{"appearance": "luminosity", "value": "tinted"}],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024",
+ },
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1,
+ },
+ }
+
+
+def adaptive_icon_contents(foreground_name: str) -> str:
+ return f"""
+
+
+
+
+
+"""
+
+
+def splash_theme_name(ios_name: str) -> str:
+ if ios_name == "AppIcon":
+ return "Theme.Nuvio.Splash"
+ return f"Theme.Nuvio.Splash.{ios_name.removeprefix('AppIcon')}"
+
+
+def base_theme_contents() -> str:
+ alternate_themes = "\n".join(
+ f' '
+ for _, _, ios_name, _ in VARIANTS[1:]
+ )
+ return f"""
+
+
+
+
+{alternate_themes}
+
+"""
+
+
+def v31_theme_contents() -> str:
+ alternate_themes = "\n\n".join(
+ f""" """
+ for key, _, ios_name, _ in VARIANTS[1:]
+ )
+ return f"""
+
+
+
+
+
+{alternate_themes}
+
+"""
+
+
+def remove_retired_assets() -> None:
+ for key, _, ios_name, _ in ALL_VARIANTS:
+ if key in SHORTLIST_KEYS:
+ continue
+ (COMPOSE_DRAWABLE / f"app_icon_{key}.png").unlink(missing_ok=True)
+ (COMPOSE_DRAWABLE / f"app_logo_wordmark_{key}.png").unlink(missing_ok=True)
+ (ANDROID_SPLASH / f"ic_splash_logo_{key}.webp").unlink(missing_ok=True)
+ (ANDROID_RES / "mipmap-anydpi-v26" / f"ic_launcher_{key}.xml").unlink(
+ missing_ok=True,
+ )
+ for density in DENSITIES:
+ directory = ANDROID_RES / f"mipmap-{density}"
+ (directory / f"ic_launcher_{key}.webp").unlink(missing_ok=True)
+ (directory / f"ic_launcher_{key}_foreground.webp").unlink(missing_ok=True)
+ shutil.rmtree(IOS_ASSETS / f"{ios_name}.appiconset", ignore_errors=True)
+
+
+def generate_preview_board(icons: list[tuple[str, Image.Image]]) -> None:
+ columns = 6
+ tile_width = 220
+ tile_height = 244
+ margin = 32
+ header_height = 104
+ rows = (len(icons) + columns - 1) // columns
+ board = Image.new(
+ "RGB",
+ (margin * 2 + columns * tile_width, header_height + margin + rows * tile_height),
+ (8, 8, 10),
+ )
+ draw = ImageDraw.Draw(board)
+ title_font = ImageFont.truetype(FONT, 34)
+ label_font = ImageFont.truetype(FONT, 18)
+ draw.text((margin, 28), "NUVIO / APP ICON CATALOGUE", fill=(245, 245, 247), font=title_font)
+ draw.text((margin, 70), "6 shortlisted colorways · pure black field", fill=(150, 150, 158), font=label_font)
+
+ for index, (label, icon) in enumerate(icons):
+ row = index // columns
+ column = index % columns
+ x = margin + column * tile_width
+ y = header_height + row * tile_height
+ draw.rounded_rectangle(
+ (x + 6, y + 6, x + tile_width - 6, y + tile_height - 8),
+ radius=24,
+ fill=(17, 17, 20),
+ outline=(42, 42, 47),
+ width=1,
+ )
+ preview = resized(icon, 168)
+ board.paste(preview, (x + 26, y + 22))
+ text_bounds = draw.textbbox((0, 0), label, font=label_font)
+ text_width = text_bounds[2] - text_bounds[0]
+ draw.text(
+ (x + (tile_width - text_width) / 2, y + 204),
+ label,
+ fill=(232, 232, 237),
+ font=label_font,
+ )
+
+ PREVIEW_BOARD.parent.mkdir(parents=True, exist_ok=True)
+ board.save(PREVIEW_BOARD, "PNG", optimize=True)
+
+
+def generate() -> None:
+ COMPOSE_DRAWABLE.mkdir(parents=True, exist_ok=True)
+ remove_retired_assets()
+ previews = []
+
+ for key, filename, ios_name, label in VARIANTS:
+ transparent = Image.open(COLORWAYS / "transparent" / filename).convert("RGBA")
+ expanded = expanded_logo(transparent, max_width_ratio=0.70, max_height_ratio=0.74)
+ adaptive = expanded_logo(transparent, max_width_ratio=0.52, max_height_ratio=0.55)
+ black = on_black(expanded)
+ launcher_name = "ic_launcher" if key == "original" else f"ic_launcher_{key}"
+ foreground_name = f"{launcher_name}_foreground"
+ splash_name = "ic_splash_logo" if key == "original" else f"ic_splash_logo_{key}"
+ previews.append((label, black))
+
+ save_webp(transparent, ANDROID_SPLASH / f"{splash_name}.webp")
+
+ adaptive_directory = ANDROID_RES / "mipmap-anydpi-v26"
+ adaptive_directory.mkdir(parents=True, exist_ok=True)
+ (adaptive_directory / f"{launcher_name}.xml").write_text(
+ adaptive_icon_contents(foreground_name),
+ encoding="utf-8",
+ )
+
+ resized(black, 256).save(
+ COMPOSE_DRAWABLE / f"app_icon_{key}.png",
+ "PNG",
+ optimize=True,
+ )
+ wordmark_for(transparent).save(
+ COMPOSE_DRAWABLE / f"app_logo_wordmark_{key}.png",
+ "PNG",
+ optimize=True,
+ )
+
+ icon_set = IOS_ASSETS / f"{ios_name}.appiconset"
+ icon_set.mkdir(parents=True, exist_ok=True)
+ resized(black, 1024).save(icon_set / "app-icon-1024.png", "PNG", optimize=True)
+ (icon_set / "Contents.json").write_text(
+ json.dumps(ios_contents(), indent=2) + "\n",
+ encoding="utf-8",
+ )
+
+ for density, scale in DENSITIES.items():
+ directory = ANDROID_RES / f"mipmap-{density}"
+ legacy_size = round(48 * scale)
+ foreground_size = round(108 * scale)
+ save_webp(resized(black, legacy_size), directory / f"{launcher_name}.webp")
+ save_webp(
+ resized(adaptive, foreground_size),
+ directory / f"{foreground_name}.webp",
+ )
+
+ if key == "original":
+ save_webp(
+ round_icon(resized(black, legacy_size)),
+ directory / "ic_launcher_round.webp",
+ )
+ save_webp(
+ resized(monochrome_icon(adaptive), foreground_size),
+ directory / "ic_launcher_monochrome.webp",
+ )
+
+ generate_preview_board(previews)
+ ANDROID_THEMES.write_text(base_theme_contents(), encoding="utf-8")
+ ANDROID_THEMES_V31.write_text(v31_theme_contents(), encoding="utf-8")
+
+
+if __name__ == "__main__":
+ generate()
diff --git a/scripts/generate_logo_colorways.py b/scripts/generate_logo_colorways.py
new file mode 100644
index 000000000..566a59202
--- /dev/null
+++ b/scripts/generate_logo_colorways.py
@@ -0,0 +1,486 @@
+from pathlib import Path
+import colorsys
+
+import cv2
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont
+
+
+ROOT = Path(__file__).resolve().parents[1]
+SOURCE = ROOT / "asset/Deliverables 2/Logo Only/Logo_1080x1080.png"
+OUTPUT = ROOT / "asset/Logo Colorways - Black"
+FONT_REGULAR = Path("/System/Library/Fonts/SFNS.ttf")
+BLACK = "#000000"
+
+
+FAMILIES = [
+ {
+ "slug": "core-cool",
+ "name": "Core Cool",
+ "label": "CORE\nCOOL",
+ "description": "Brand-first\ntechnology palette",
+ },
+ {
+ "slug": "aqua-green",
+ "name": "Aqua + Green",
+ "label": "AQUA +\nGREEN",
+ "description": "Fresh product and\ngrowth palette",
+ },
+ {
+ "slug": "warm-spectrum",
+ "name": "Warm Spectrum",
+ "label": "WARM\nSPECTRUM",
+ "description": "Entertainment and\ncampaign palette",
+ },
+ {
+ "slug": "luxe-neutral",
+ "name": "Luxe + Neutral",
+ "label": "LUXE +\nNEUTRAL",
+ "description": "Premium and\nsystem palette",
+ },
+]
+
+
+VARIANTS = [
+ {
+ "family": "core-cool",
+ "slug": "original-on-black",
+ "name": "Original on Black",
+ "swatches": ["#45D6EB", "#3965D4", "#B250EF"],
+ "hues": [190, 225, 286],
+ "usage": "Current identity",
+ "preserve": True,
+ },
+ {
+ "family": "core-cool",
+ "slug": "glacier",
+ "name": "Glacier",
+ "swatches": ["#7BE7FF", "#35B9FF", "#506BFF"],
+ "hues": [191, 202, 231],
+ "usage": "Bright and approachable",
+ },
+ {
+ "family": "core-cool",
+ "slug": "arctic-blue",
+ "name": "Arctic Blue",
+ "swatches": ["#4DE3FF", "#3185F5", "#4D55E8"],
+ "hues": [188, 214, 237],
+ "usage": "Best master-brand option",
+ "recommended": True,
+ },
+ {
+ "family": "core-cool",
+ "slug": "cobalt-pulse",
+ "name": "Cobalt Pulse",
+ "swatches": ["#59A8FF", "#315BFF", "#613DFF"],
+ "hues": [211, 228, 254],
+ "usage": "High-energy technology",
+ },
+ {
+ "family": "core-cool",
+ "slug": "sapphire",
+ "name": "Sapphire",
+ "swatches": ["#38C5FF", "#2265E5", "#3B2FD1"],
+ "hues": [196, 219, 245],
+ "usage": "Confident and premium",
+ },
+ {
+ "family": "core-cool",
+ "slug": "electric-violet",
+ "name": "Electric Violet",
+ "swatches": ["#4985FF", "#704DFF", "#EA4DFF"],
+ "hues": [218, 253, 293],
+ "usage": "Creator-led media",
+ },
+ {
+ "family": "aqua-green",
+ "slug": "polar-mint",
+ "name": "Polar Mint",
+ "swatches": ["#80FFE3", "#31E6C5", "#21B8E5"],
+ "hues": [167, 169, 194],
+ "usage": "Light and contemporary",
+ },
+ {
+ "family": "aqua-green",
+ "slug": "tidal",
+ "name": "Tidal",
+ "swatches": ["#42E8B4", "#14C8C8", "#2384E8"],
+ "hues": [160, 180, 210],
+ "usage": "Best fresh-brand option",
+ "recommended": True,
+ },
+ {
+ "family": "aqua-green",
+ "slug": "turquoise",
+ "name": "Turquoise",
+ "swatches": ["#38F2D0", "#12D6B3", "#19A7D8"],
+ "hues": [169, 168, 195],
+ "usage": "Clean and global",
+ },
+ {
+ "family": "aqua-green",
+ "slug": "emerald",
+ "name": "Emerald",
+ "swatches": ["#7BF08D", "#22D37C", "#0BBF9A"],
+ "hues": [127, 148, 168],
+ "usage": "Mature and assured",
+ },
+ {
+ "family": "aqua-green",
+ "slug": "jade",
+ "name": "Jade",
+ "swatches": ["#B9F44A", "#2DDB83", "#12B8A6"],
+ "hues": [79, 150, 174],
+ "usage": "Energetic product growth",
+ },
+ {
+ "family": "aqua-green",
+ "slug": "lime-pulse",
+ "name": "Lime Pulse",
+ "swatches": ["#D5FF4F", "#78ED3B", "#16C995"],
+ "hues": [72, 104, 157],
+ "usage": "Youth and activation",
+ },
+ {
+ "family": "warm-spectrum",
+ "slug": "sunset",
+ "name": "Sunset",
+ "swatches": ["#8D55FF", "#F044A7", "#FF7048"],
+ "hues": [260, 324, 374],
+ "usage": "Best campaign option",
+ "recommended": True,
+ },
+ {
+ "family": "warm-spectrum",
+ "slug": "magenta-rush",
+ "name": "Magenta Rush",
+ "swatches": ["#6B5CFF", "#E640D5", "#FF4E87"],
+ "hues": [248, 306, 339],
+ "usage": "Creator campaigns",
+ },
+ {
+ "family": "warm-spectrum",
+ "slug": "cherry-pop",
+ "name": "Cherry Pop",
+ "swatches": ["#FF4AB8", "#FF3D6E", "#FF633D"],
+ "hues": [326, 344, 373],
+ "usage": "Bold social presence",
+ },
+ {
+ "family": "warm-spectrum",
+ "slug": "crimson",
+ "name": "Crimson",
+ "swatches": ["#FF5B91", "#F52E62", "#E42E3D"],
+ "hues": [340, 346, 355],
+ "usage": "Dramatic editorial use",
+ },
+ {
+ "family": "warm-spectrum",
+ "slug": "ember-gold",
+ "name": "Ember Gold",
+ "swatches": ["#FF476F", "#FF7738", "#FFD34E"],
+ "hues": [347, 379, 407],
+ "usage": "Events and launches",
+ },
+ {
+ "family": "warm-spectrum",
+ "slug": "solar",
+ "name": "Solar",
+ "swatches": ["#FF6B3D", "#FFAA2D", "#FFE35C"],
+ "hues": [14, 36, 50],
+ "usage": "Optimistic promotion",
+ },
+ {
+ "family": "luxe-neutral",
+ "slug": "rose-gold",
+ "name": "Rose Gold",
+ "swatches": ["#B75AFF", "#EC70A9", "#FFB37A"],
+ "hues": [279, 334, 385],
+ "usage": "Lifestyle premium",
+ },
+ {
+ "family": "luxe-neutral",
+ "slug": "amethyst",
+ "name": "Amethyst",
+ "swatches": ["#6E5BFF", "#A85CFF", "#E475FF"],
+ "hues": [247, 273, 291],
+ "usage": "Best premium option",
+ "recommended": True,
+ },
+ {
+ "family": "luxe-neutral",
+ "slug": "burgundy",
+ "name": "Burgundy",
+ "swatches": ["#B63D82", "#D94D70", "#ED765C"],
+ "hues": [326, 345, 369],
+ "usage": "Cinematic and editorial",
+ },
+ {
+ "family": "luxe-neutral",
+ "slug": "copper",
+ "name": "Copper",
+ "swatches": ["#C95945", "#DE7B45", "#ECAE62"],
+ "hues": [9, 24, 38],
+ "usage": "Warm heritage premium",
+ },
+ {
+ "family": "luxe-neutral",
+ "slug": "champagne",
+ "name": "Champagne",
+ "swatches": ["#D6A95F", "#E8C979", "#F7E6B0"],
+ "hues": [38, 44, 46],
+ "usage": "Luxury partnerships",
+ },
+ {
+ "family": "luxe-neutral",
+ "slug": "graphite",
+ "name": "Graphite",
+ "swatches": ["#F3F5F7", "#AAB2BE", "#687381"],
+ "hues": [210, 215, 220],
+ "usage": "Monochrome system mark",
+ "monochrome": True,
+ },
+]
+
+
+def hex_rgb(value):
+ value = value.lstrip("#")
+ return tuple(int(value[index:index + 2], 16) for index in (0, 2, 4))
+
+
+def mix_hex(first, second, amount):
+ a = np.array(hex_rgb(first), dtype=np.float32)
+ b = np.array(hex_rgb(second), dtype=np.float32)
+ return tuple(np.rint(a * (1 - amount) + b * amount).astype(np.uint8))
+
+
+def swatch_hsv(swatches):
+ converted = []
+ for value in swatches:
+ red, green, blue = (channel / 255 for channel in hex_rgb(value))
+ converted.append(colorsys.rgb_to_hsv(red, green, blue))
+ return np.array(converted, dtype=np.float32)
+
+
+def recolor(source, variant):
+ pixels = np.array(source.convert("RGBA"), dtype=np.uint8)
+ rgb = pixels[..., :3]
+ alpha = pixels[..., 3]
+ hsv = cv2.cvtColor(rgb.astype(np.float32) / 255, cv2.COLOR_RGB2HSV)
+ hue = hsv[..., 0]
+ saturation = hsv[..., 1]
+ value = hsv[..., 2]
+ mapped_rgb = rgb.copy()
+ if not variant.get("preserve"):
+ color_mask = (alpha > 0) & (saturation > 0.12) & (value > 0.22)
+ position = np.clip((hue - 184) / 110, 0, 1)
+ stops = np.array([0, 0.5, 1], dtype=np.float32)
+ target_hue = np.interp(position, stops, variant["hues"]) % 360
+ targets = swatch_hsv(variant["swatches"])
+ target_saturation = np.interp(position, stops, targets[:, 1])
+ target_value = np.interp(position, stops, targets[:, 2])
+ saturation_factor = np.clip((saturation / 0.69) ** 0.35, 0.72, 1.16)
+ value_factor = np.clip((value / 0.88) ** 0.65, 0.74, 1.1)
+ mapped_saturation = np.clip(target_saturation * saturation_factor, 0, 1)
+ if variant.get("monochrome"):
+ mapped_saturation = np.zeros_like(mapped_saturation)
+ mapped_value = np.clip(target_value * value_factor, 0, 1)
+ adjusted = hsv.copy()
+ adjusted[..., 0][color_mask] = target_hue[color_mask]
+ adjusted[..., 1][color_mask] = mapped_saturation[color_mask]
+ adjusted[..., 2][color_mask] = mapped_value[color_mask]
+ mapped_rgb = np.rint(cv2.cvtColor(adjusted, cv2.COLOR_HSV2RGB) * 255).astype(np.uint8)
+ dark_mask = (alpha > 0) & (value <= 0.22)
+ dark_level = np.clip(value / 0.118, 0.58, 1.35)
+ inner = np.array(hex_rgb(BLACK), dtype=np.float32)
+ mapped_rgb[dark_mask] = np.clip(inner * dark_level[dark_mask, None], 0, 255).astype(np.uint8)
+ pixels[..., :3] = mapped_rgb
+ return Image.fromarray(pixels, mode="RGBA")
+
+
+def composite_black(logo):
+ background = Image.new("RGBA", logo.size, (0, 0, 0, 255))
+ background.alpha_composite(logo)
+ return background.convert("RGB")
+
+
+def fit_logo(logo, width, height):
+ bounds = logo.getchannel("A").getbbox()
+ cropped = logo.crop(bounds)
+ scale = min(width / cropped.width, height / cropped.height)
+ size = (round(cropped.width * scale), round(cropped.height * scale))
+ return cropped.resize(size, Image.Resampling.LANCZOS)
+
+
+def font(size):
+ return ImageFont.truetype(str(FONT_REGULAR), size)
+
+
+def family_variants(slug):
+ return [variant for variant in VARIANTS if variant["family"] == slug]
+
+
+def draw_swatch_row(draw, swatches, left, top, diameter, gap):
+ x = left
+ for swatch in swatches:
+ draw.ellipse(
+ (x, top, x + diameter, top + diameter),
+ fill=swatch,
+ outline=mix_hex(swatch, "#FFFFFF", 0.38),
+ width=2,
+ )
+ x += diameter + gap
+
+
+def draw_master(rendered):
+ canvas_width = 3080
+ margin = 60
+ rail_width = 250
+ header_height = 220
+ card_width = 430
+ card_height = 420
+ column_gap = 20
+ row_gap = 30
+ canvas_height = header_height + card_height * 4 + row_gap * 3 + margin
+ canvas = Image.new("RGB", (canvas_width, canvas_height), BLACK)
+ draw = ImageDraw.Draw(canvas)
+ draw.text((margin, 42), "NUVIO / COLOR SYSTEM", font=font(58), fill="#F7F7F7")
+ draw.text(
+ (margin, 120),
+ "24 curated logo colorways · pure black preview field · original geometry preserved",
+ font=font(26),
+ fill="#909090",
+ )
+ rendered_by_slug = {variant["slug"]: logo for variant, logo in zip(VARIANTS, rendered)}
+ for family_index, family in enumerate(FAMILIES):
+ top = header_height + family_index * (card_height + row_gap)
+ draw.multiline_text(
+ (margin, top + 92),
+ family["label"],
+ font=font(34),
+ fill="#F7F7F7",
+ spacing=4,
+ )
+ draw.multiline_text(
+ (margin, top + 188),
+ family["description"],
+ font=font(19),
+ fill="#777777",
+ spacing=4,
+ )
+ for column, variant in enumerate(family_variants(family["slug"])):
+ left = margin + rail_width + column * (card_width + column_gap)
+ right = left + card_width
+ bottom = top + card_height
+ border = "#323232" if variant.get("recommended") else "#222222"
+ draw.rounded_rectangle((left, top, right, bottom), radius=24, fill=BLACK, outline=border, width=2)
+ logo = fit_logo(rendered_by_slug[variant["slug"]], 285, 285)
+ logo_x = left + (card_width - logo.width) // 2
+ canvas.paste(logo, (logo_x, top + 18), logo)
+ draw.text((left + 24, top + 314), variant["name"], font=font(25), fill="#F4F4F4")
+ draw.text((left + 24, top + 350), variant["usage"], font=font(16), fill="#777777")
+ draw_swatch_row(draw, variant["swatches"], left + 24, top + 382, 22, 8)
+ if variant.get("recommended"):
+ badge_width = 116
+ badge_left = right - badge_width - 18
+ draw.rounded_rectangle(
+ (badge_left, top + 374, right - 18, top + 402),
+ radius=14,
+ fill="#F4F4F4",
+ )
+ draw.text((badge_left + 12, top + 379), "DESIGNER PICK", font=font(12), fill=BLACK)
+ canvas.save(OUTPUT / "Nuvio-Color-System-Master.png", optimize=True)
+
+
+def draw_family_board(family, rendered_by_slug):
+ canvas_width = 2280
+ margin = 60
+ gap = 30
+ header_height = 220
+ card_width = 700
+ card_height = 560
+ canvas_height = header_height + card_height * 2 + gap + margin
+ canvas = Image.new("RGB", (canvas_width, canvas_height), BLACK)
+ draw = ImageDraw.Draw(canvas)
+ draw.text((margin, 44), f"NUVIO / {family['name'].upper()}", font=font(58), fill="#F7F7F7")
+ draw.text(
+ (margin, 124),
+ "Six curated logo shades shown on pure black #000000",
+ font=font(26),
+ fill="#909090",
+ )
+ for index, variant in enumerate(family_variants(family["slug"])):
+ row, column = divmod(index, 3)
+ left = margin + column * (card_width + gap)
+ top = header_height + row * (card_height + gap)
+ right = left + card_width
+ bottom = top + card_height
+ border = "#3A3A3A" if variant.get("recommended") else "#242424"
+ draw.rounded_rectangle((left, top, right, bottom), radius=32, fill=BLACK, outline=border, width=2)
+ logo = fit_logo(rendered_by_slug[variant["slug"]], 390, 390)
+ logo_x = left + (card_width - logo.width) // 2
+ canvas.paste(logo, (logo_x, top + 20), logo)
+ draw.text((left + 34, top + 420), variant["name"], font=font(32), fill="#F7F7F7")
+ draw.text((left + 34, top + 464), variant["usage"], font=font(19), fill="#838383")
+ draw_swatch_row(draw, variant["swatches"], right - 34 - 138, top + 458, 38, 12)
+ if variant.get("recommended"):
+ draw.rounded_rectangle((left + 34, top + 504, left + 174, top + 536), radius=16, fill="#F2F2F2")
+ draw.text((left + 48, top + 510), "DESIGNER PICK", font=font(14), fill=BLACK)
+ canvas.save(OUTPUT / "previews" / f"{family['slug']}.png", optimize=True)
+
+
+def write_guide():
+ lines = [
+ "# Nuvio logo color system",
+ "",
+ "This set contains 24 professionally curated colorways across four usage families. Every preview uses pure black `#000000`. Logo geometry, transparency, highlights, overlap shading, and the white play glyph are preserved.",
+ "",
+ "## Designer recommendation",
+ "",
+ "- Use **Arctic Blue** as the strongest evolution of the master brand.",
+ "- Use **Tidal** when the product needs a fresher, more distinctive identity.",
+ "- Use **Amethyst** for premium positioning and **Graphite** for monochrome system contexts.",
+ "- Reserve **Sunset**, **Ember Gold**, and the brighter warm variants for campaigns, launches, and seasonal moments.",
+ "",
+ "| Family | Colorway | Gradient anchors | Recommended use |",
+ "| --- | --- | --- | --- |",
+ ]
+ family_names = {family["slug"]: family["name"] for family in FAMILIES}
+ for variant in VARIANTS:
+ anchors = " → ".join(variant["swatches"])
+ lines.append(f"| {family_names[variant['family']]} | {variant['name']} | {anchors} | {variant['usage']} |")
+ lines.extend(
+ [
+ "",
+ "Each colorway is supplied as a transparent 1080×1080 PNG and a matching 1080×1080 PNG on pure black.",
+ "",
+ "The source is `asset/Deliverables 2/Logo Only/Logo_1080x1080.png`.",
+ ]
+ )
+ (OUTPUT / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+def main():
+ transparent_dir = OUTPUT / "transparent"
+ black_dir = OUTPUT / "black-background"
+ preview_dir = OUTPUT / "previews"
+ transparent_dir.mkdir(parents=True, exist_ok=True)
+ black_dir.mkdir(parents=True, exist_ok=True)
+ preview_dir.mkdir(parents=True, exist_ok=True)
+ source = Image.open(SOURCE).convert("RGBA")
+ rendered = []
+ for index, variant in enumerate(VARIANTS, start=1):
+ logo = recolor(source, variant)
+ filename = f"{index:02d}-{variant['slug']}.png"
+ logo.save(transparent_dir / filename, optimize=True)
+ composite_black(logo).save(black_dir / filename, optimize=True)
+ rendered.append(logo)
+ rendered_by_slug = {variant["slug"]: logo for variant, logo in zip(VARIANTS, rendered)}
+ draw_master(rendered)
+ for family in FAMILIES:
+ draw_family_board(family, rendered_by_slug)
+ write_guide()
+
+
+if __name__ == "__main__":
+ main()