diff --git a/i18n/en.json b/i18n/en.json
index 7a3e719f6d..3741722585 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -557,6 +557,18 @@
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_title": "Sign out",
"app_download_links": "App Download Links",
+ "app_icon_blossom": "Blossom",
+ "app_icon_change_failed": "Unable to change the app icon",
+ "app_icon_classic": "Classic",
+ "app_icon_forest": "Forest",
+ "app_icon_gold": "Gold",
+ "app_icon_ink": "Ink",
+ "app_icon_midnight": "Midnight",
+ "app_icon_neon": "Neon",
+ "app_icon_ocean": "Ocean",
+ "app_icon_subtitle": "Choose the icon shown on your home screen",
+ "app_icon_sunset": "Sunset",
+ "app_icon_title": "App Icon",
"app_settings": "App Settings",
"app_stores": "App Stores",
"app_update_available": "App update is available",
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index 1b8d2a97fb..be413a113a 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -56,11 +56,6 @@
Window background behind the Flutter UI. -->
-
-
-
-
-
@@ -137,6 +132,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ return listOf(result)
+ }
+
+ fun wrapError(exception: Throwable): List {
+ return if (exception is FlutterError) {
+ listOf(
+ exception.code,
+ exception.message,
+ exception.details
+ )
+ } else {
+ listOf(
+ exception.javaClass.simpleName,
+ exception.toString(),
+ "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
+ )
+ }
+ }
+}
+
+/**
+ * Error class for passing custom error details to Flutter via a thrown PlatformException.
+ * @property code The error code.
+ * @property message The error message.
+ * @property details The error details. Must be a datatype supported by the api codec.
+ */
+class FlutterError (
+ val code: String,
+ override val message: String? = null,
+ val details: Any? = null
+) : RuntimeException()
+private open class AppIconPigeonCodec : StandardMessageCodec() {
+ override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
+ return super.readValueOfType(type, buffer)
+ }
+ override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
+ super.writeValue(stream, value)
+ }
+}
+
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface AppIconApi {
+ fun setAppIcon(iconId: String, callback: (Result) -> Unit)
+ fun getAppIcon(): String
+
+ companion object {
+ /** The codec used by AppIconApi. */
+ val codec: MessageCodec by lazy {
+ AppIconPigeonCodec()
+ }
+ /** Sets up an instance of `AppIconApi` to handle messages through the `binaryMessenger`. */
+ @JvmOverloads
+ fun setUp(binaryMessenger: BinaryMessenger, api: AppIconApi?, messageChannelSuffix: String = "") {
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AppIconApi.setAppIcon$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val iconIdArg = args[0] as String
+ api.setAppIcon(iconIdArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(AppIconPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(AppIconPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.AppIconApi.getAppIcon$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getAppIcon())
+ } catch (exception: Throwable) {
+ AppIconPigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/appicon/AppIconApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/appicon/AppIconApiImpl.kt
new file mode 100644
index 0000000000..27d45f3136
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/appicon/AppIconApiImpl.kt
@@ -0,0 +1,55 @@
+package app.alextran.immich.appicon
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+
+/**
+ * Switches the launcher icon by toggling the `.AppIcon*` activity-aliases
+ * declared in the AndroidManifest. Exactly one alias is enabled at a time;
+ * the aliases all target MainActivity.
+ */
+class AppIconApiImpl(context: Context) : AppIconApi {
+ private val ctx: Context = context.applicationContext
+
+ companion object {
+ private const val DEFAULT_ICON = "classic"
+ private val ICONS =
+ listOf("classic", "midnight", "ocean", "sunset", "forest", "blossom", "ink", "neon", "gold")
+ }
+
+ private fun componentFor(iconId: String): ComponentName {
+ val alias = "AppIcon" + iconId.replaceFirstChar { it.uppercase() }
+ return ComponentName(ctx.packageName, "app.alextran.immich.$alias")
+ }
+
+ override fun setAppIcon(iconId: String, callback: (Result) -> Unit) {
+ if (iconId !in ICONS) {
+ callback(Result.failure(FlutterError("invalid_icon", "Unknown app icon: $iconId")))
+ return
+ }
+
+ val pm = ctx.packageManager
+ for (icon in ICONS) {
+ val state = if (icon == iconId) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ }
+ pm.setComponentEnabledSetting(componentFor(icon), state, PackageManager.DONT_KILL_APP)
+ }
+ callback(Result.success(Unit))
+ }
+
+ override fun getAppIcon(): String {
+ val pm = ctx.packageManager
+ for (icon in ICONS) {
+ if (pm.getComponentEnabledSetting(componentFor(icon)) ==
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ ) {
+ return icon
+ }
+ }
+ return DEFAULT_ICON
+ }
+}
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_blossom.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_blossom.xml
new file mode 100644
index 0000000000..0f3c7dec53
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_blossom.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_forest.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_forest.xml
new file mode 100644
index 0000000000..e8c8e1fc36
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_forest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_gold.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_gold.xml
new file mode 100644
index 0000000000..ef94097c42
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_gold.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_ink.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_ink.xml
new file mode 100644
index 0000000000..4f93f9822b
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_ink.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_midnight.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_midnight.xml
new file mode 100644
index 0000000000..d14fce37ea
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_midnight.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_neon.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_neon.xml
new file mode 100644
index 0000000000..088605e0d7
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_neon.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_ocean.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_ocean.xml
new file mode 100644
index 0000000000..fe02311ab8
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_ocean.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_sunset.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_sunset.xml
new file mode 100644
index 0000000000..8cd24718c4
--- /dev/null
+++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground_sunset.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blossom.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blossom.xml
new file mode 100644
index 0000000000..53f5ea4ea6
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_blossom.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest.xml
new file mode 100644
index 0000000000..e1bfcac7c9
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_gold.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_gold.xml
new file mode 100644
index 0000000000..0af836cba0
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_gold.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ink.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ink.xml
new file mode 100644
index 0000000000..e93e5ab90f
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ink.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_midnight.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_midnight.xml
new file mode 100644
index 0000000000..7093707247
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_midnight.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml
new file mode 100644
index 0000000000..119a62ba97
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml
new file mode 100644
index 0000000000..8d237e8439
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset.xml
new file mode 100644
index 0000000000..834347457b
--- /dev/null
+++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/res/values/app_icon_colors.xml b/mobile/android/app/src/main/res/values/app_icon_colors.xml
new file mode 100644
index 0000000000..5de795bb4a
--- /dev/null
+++ b/mobile/android/app/src/main/res/values/app_icon_colors.xml
@@ -0,0 +1,11 @@
+
+
+ #0F172A
+ #FFFFFF
+ #1E1B4B
+ #FFFFFF
+ #FFF1F2
+ #FFFFFF
+ #0A0A12
+ #1C1917
+
diff --git a/mobile/assets/app-icons/app-icon-blossom.svg b/mobile/assets/app-icons/app-icon-blossom.svg
new file mode 100644
index 0000000000..430bf7ae40
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-blossom.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-classic.svg b/mobile/assets/app-icons/app-icon-classic.svg
new file mode 100644
index 0000000000..1522e1f348
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-classic.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-forest.svg b/mobile/assets/app-icons/app-icon-forest.svg
new file mode 100644
index 0000000000..9ba3659d1c
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-forest.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-gold.svg b/mobile/assets/app-icons/app-icon-gold.svg
new file mode 100644
index 0000000000..58b297d0d6
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-gold.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-ink.svg b/mobile/assets/app-icons/app-icon-ink.svg
new file mode 100644
index 0000000000..e25b71fed6
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-ink.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-midnight.svg b/mobile/assets/app-icons/app-icon-midnight.svg
new file mode 100644
index 0000000000..32477fabc9
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-midnight.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-neon.svg b/mobile/assets/app-icons/app-icon-neon.svg
new file mode 100644
index 0000000000..5515def8b4
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-neon.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-ocean.svg b/mobile/assets/app-icons/app-icon-ocean.svg
new file mode 100644
index 0000000000..53967c027e
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-ocean.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/assets/app-icons/app-icon-sunset.svg b/mobile/assets/app-icons/app-icon-sunset.svg
new file mode 100644
index 0000000000..08af89d78d
--- /dev/null
+++ b/mobile/assets/app-icons/app-icon-sunset.svg
@@ -0,0 +1,10 @@
+
diff --git a/mobile/integration_test/app_icon_test.dart b/mobile/integration_test/app_icon_test.dart
new file mode 100644
index 0000000000..9b06d715d4
--- /dev/null
+++ b/mobile/integration_test/app_icon_test.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/platform/app_icon_api.g.dart';
+import 'package:integration_test/integration_test.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ testWidgets('switches the launcher icon and reports it back', (tester) async {
+ final api = AppIconApi();
+
+ await api.setAppIcon('neon');
+ expect(await api.getAppIcon(), 'neon');
+
+ await api.setAppIcon('classic');
+ expect(await api.getAppIcon(), 'classic');
+
+ await expectLater(api.setAppIcon('not-an-icon'), throwsA(isA()));
+ });
+}
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 5d966e0033..9d2ef9f4c9 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -16,8 +16,6 @@ PODS:
- Flutter
- native_video_player (1.0.0):
- Flutter
- - permission_handler_apple (9.3.0):
- - Flutter
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@@ -36,7 +34,6 @@ DEPENDENCIES:
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
@@ -57,8 +54,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/home_widget/ios"
native_video_player:
:path: ".symlinks/plugins/native_video_player/ios"
- permission_handler_apple:
- :path: ".symlinks/plugins/permission_handler_apple/ios"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
@@ -73,7 +68,6 @@ SPEC CHECKSUMS:
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
- permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index 96d304f3d7..fe5362b52e 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 77;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -153,11 +153,15 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B231F52D2E93A44A00BC45D1 /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ );
path = Core;
sourceTree = "";
};
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ );
path = Sync;
sourceTree = "";
};
@@ -179,6 +183,8 @@
};
FEE084F22EC172080045228E /* Schemas */ = {
isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ );
path = Schemas;
sourceTree = "";
};
@@ -370,7 +376,6 @@
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */,
- 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -464,7 +469,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
);
@@ -511,27 +516,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
- );
- inputPaths = (
- );
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
- showEnvVarsInLog = 0;
- };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -556,14 +540,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
- inputPaths = (
- );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
- outputPaths = (
- );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -772,6 +752,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -917,6 +898,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -946,6 +928,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -1261,7 +1244,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift
index e1ff3b1f6e..35baeede11 100644
--- a/mobile/ios/Runner/AppDelegate.swift
+++ b/mobile/ios/Runner/AppDelegate.swift
@@ -31,6 +31,7 @@ import native_video_player
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
ConnectivityApiSetup.setUp(binaryMessenger: messenger, api: ConnectivityApiImpl())
+ AppIconApiSetup.setUp(binaryMessenger: messenger, api: AppIconApiImpl())
NetworkApiSetup.setUp(binaryMessenger: messenger, api: NetworkApiImpl())
}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconBlossom.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconBlossom.appiconset/1024.png
new file mode 100644
index 0000000000..3284a462ed
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconBlossom.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconBlossom.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconBlossom.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconBlossom.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconForest.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconForest.appiconset/1024.png
new file mode 100644
index 0000000000..14a58f905a
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconForest.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconForest.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconForest.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconForest.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconGold.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconGold.appiconset/1024.png
new file mode 100644
index 0000000000..77f3d92f01
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconGold.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconGold.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconGold.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconGold.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconInk.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconInk.appiconset/1024.png
new file mode 100644
index 0000000000..fb5326fc00
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconInk.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconInk.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconInk.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconInk.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconMidnight.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconMidnight.appiconset/1024.png
new file mode 100644
index 0000000000..39898a6751
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconMidnight.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconMidnight.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconMidnight.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconMidnight.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconNeon.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconNeon.appiconset/1024.png
new file mode 100644
index 0000000000..e1468ee621
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconNeon.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconNeon.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconNeon.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconNeon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconOcean.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconOcean.appiconset/1024.png
new file mode 100644
index 0000000000..6f46bafb40
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconOcean.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconOcean.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconOcean.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconOcean.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconSunset.appiconset/1024.png b/mobile/ios/Runner/Assets.xcassets/AppIconSunset.appiconset/1024.png
new file mode 100644
index 0000000000..07cc6dc417
Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/AppIconSunset.appiconset/1024.png differ
diff --git a/mobile/ios/Runner/Assets.xcassets/AppIconSunset.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/AppIconSunset.appiconset/Contents.json
new file mode 100644
index 0000000000..7afd6df765
--- /dev/null
+++ b/mobile/ios/Runner/Assets.xcassets/AppIconSunset.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images": [
+ {
+ "filename": "1024.png",
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/mobile/ios/Runner/Core/AppIcon.g.swift b/mobile/ios/Runner/Core/AppIcon.g.swift
new file mode 100644
index 0000000000..1b91c6d984
--- /dev/null
+++ b/mobile/ios/Runner/Core/AppIcon.g.swift
@@ -0,0 +1,114 @@
+// Autogenerated from Pigeon (v26.3.4), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+import Foundation
+
+#if os(iOS)
+ import Flutter
+#elseif os(macOS)
+ import FlutterMacOS
+#else
+ #error("Unsupported platform.")
+#endif
+
+private func wrapResult(_ result: Any?) -> [Any?] {
+ return [result]
+}
+
+private func wrapError(_ error: Any) -> [Any?] {
+ if let pigeonError = error as? PigeonError {
+ return [
+ pigeonError.code,
+ pigeonError.message,
+ pigeonError.details,
+ ]
+ }
+ if let flutterError = error as? FlutterError {
+ return [
+ flutterError.code,
+ flutterError.message,
+ flutterError.details,
+ ]
+ }
+ return [
+ "\(error)",
+ "\(Swift.type(of: error))",
+ "Stacktrace: \(Thread.callStackSymbols)",
+ ]
+}
+
+private func isNullish(_ value: Any?) -> Bool {
+ return value is NSNull || value == nil
+}
+
+private func nilOrValue(_ value: Any?) -> T? {
+ if value is NSNull { return nil }
+ return value as! T?
+}
+
+
+private class AppIconPigeonCodecReader: FlutterStandardReader {
+}
+
+private class AppIconPigeonCodecWriter: FlutterStandardWriter {
+}
+
+private class AppIconPigeonCodecReaderWriter: FlutterStandardReaderWriter {
+ override func reader(with data: Data) -> FlutterStandardReader {
+ return AppIconPigeonCodecReader(data: data)
+ }
+
+ override func writer(with data: NSMutableData) -> FlutterStandardWriter {
+ return AppIconPigeonCodecWriter(data: data)
+ }
+}
+
+class AppIconPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
+ static let shared = AppIconPigeonCodec(readerWriter: AppIconPigeonCodecReaderWriter())
+}
+
+
+/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
+protocol AppIconApi {
+ func setAppIcon(iconId: String, completion: @escaping (Result) -> Void)
+ func getAppIcon() throws -> String
+}
+
+/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
+class AppIconApiSetup {
+ static var codec: FlutterStandardMessageCodec { AppIconPigeonCodec.shared }
+ /// Sets up an instance of `AppIconApi` to handle messages through the `binaryMessenger`.
+ static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AppIconApi?, messageChannelSuffix: String = "") {
+ let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
+ let setAppIconChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AppIconApi.setAppIcon\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ setAppIconChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let iconIdArg = args[0] as! String
+ api.setAppIcon(iconId: iconIdArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ setAppIconChannel.setMessageHandler(nil)
+ }
+ let getAppIconChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.AppIconApi.getAppIcon\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getAppIconChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getAppIcon()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getAppIconChannel.setMessageHandler(nil)
+ }
+ }
+}
diff --git a/mobile/ios/Runner/Core/AppIconApiImpl.swift b/mobile/ios/Runner/Core/AppIconApiImpl.swift
new file mode 100644
index 0000000000..fdeb7c6859
--- /dev/null
+++ b/mobile/ios/Runner/Core/AppIconApiImpl.swift
@@ -0,0 +1,45 @@
+import UIKit
+
+/// Switches between the alternate app icons bundled in the asset catalog
+/// (`AppIconMidnight`, `AppIconOcean`, ...). The "classic" id maps to the
+/// primary `AppIcon`.
+class AppIconApiImpl: AppIconApi {
+ private static let defaultIcon = "classic"
+ private static let assetPrefix = "AppIcon"
+
+ private func assetName(for iconId: String) -> String? {
+ guard iconId != Self.defaultIcon else { return nil }
+ return Self.assetPrefix + iconId.prefix(1).uppercased() + String(iconId.dropFirst())
+ }
+
+ func setAppIcon(iconId: String, completion: @escaping (Result) -> Void) {
+ let iconName = assetName(for: iconId)
+ DispatchQueue.main.async {
+ guard UIApplication.shared.supportsAlternateIcons else {
+ completion(
+ .failure(
+ PigeonError(
+ code: "unsupported",
+ message: "Alternate icons are not supported on this device",
+ details: nil
+ )))
+ return
+ }
+ UIApplication.shared.setAlternateIconName(iconName) { error in
+ if let error = error {
+ completion(
+ .failure(
+ PigeonError(code: "set_icon_failed", message: error.localizedDescription, details: nil)
+ ))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
+ }
+
+ func getAppIcon() throws -> String {
+ guard let name = UIApplication.shared.alternateIconName else { return Self.defaultIcon }
+ return String(name.dropFirst(Self.assetPrefix.count)).lowercased()
+ }
+}
diff --git a/mobile/lib/constants/app_icons.dart b/mobile/lib/constants/app_icons.dart
new file mode 100644
index 0000000000..8d10664167
--- /dev/null
+++ b/mobile/lib/constants/app_icons.dart
@@ -0,0 +1,21 @@
+/// The selectable launcher icons. The [name] of each variant is the id shared
+/// with the native implementations (Android activity-aliases and iOS
+/// alternate icon asset names), so renaming a value is a breaking change.
+enum AppIconVariant {
+ classic,
+ midnight,
+ ocean,
+ sunset,
+ forest,
+ blossom,
+ ink,
+ neon,
+ gold;
+
+ String get assetPath => 'assets/app-icons/app-icon-$name.svg';
+
+ String get translationKey => 'app_icon_$name';
+
+ static AppIconVariant fromId(String id) =>
+ AppIconVariant.values.firstWhere((variant) => variant.name == id, orElse: () => AppIconVariant.classic);
+}
diff --git a/mobile/lib/platform/app_icon_api.g.dart b/mobile/lib/platform/app_icon_api.g.dart
new file mode 100644
index 0000000000..41f13f8217
--- /dev/null
+++ b/mobile/lib/platform/app_icon_api.g.dart
@@ -0,0 +1,109 @@
+// Autogenerated from Pigeon (v26.3.4), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: unused_import, unused_shown_name
+// ignore_for_file: type=lint
+
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List;
+
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
+
+Object? _extractReplyValueOrThrow(
+ List