diff --git a/localizely.yml b/localizely.yml
index 86b4b077d8..76feea21eb 100644
--- a/localizely.yml
+++ b/localizely.yml
@@ -1,78 +1,94 @@
-config_version: 1.0
-project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
-file_type: json
-branch: main
-upload:
- files:
- - file: mobile/assets/i18n/en-US.json
- locale_code: en-US
-download:
- params:
- export_empty_as: main
- files:
- - file: mobile/assets/i18n/en-US.json
- locale_code: en-US
- - file: mobile/assets/i18n/de-DE.json
- locale_code: de-DE
- - file: mobile/assets/i18n/da-DK.json
- locale_code: da-DK
- - file: mobile/assets/i18n/it-IT.json
- locale_code: it-IT
- - file: mobile/assets/i18n/es-ES.json
- locale_code: es-ES
- - file: mobile/assets/i18n/vi-VN.json
- locale_code: vi-VN
- - file: mobile/assets/i18n/fr-FR.json
- locale_code: fr-FR
- - file: mobile/assets/i18n/ja-JP.json
- locale_code: ja-JP
- - file: mobile/assets/i18n/pl-PL.json
- locale_code: pl-PL
- - file: mobile/assets/i18n/fi-FI.json
- locale_code: fi-FI
- - file: mobile/assets/i18n/pt-PT.json
- locale_code: pt-PT
- - file: mobile/assets/i18n/pt-BR.json
- locale_code: pt-BR
- - file: mobile/assets/i18n/cs-CZ.json
- locale_code: cs-CZ
- - file: mobile/assets/i18n/uk-UA.json
- locale_code: uk-UA
- - file: mobile/assets/i18n/ru-RU.json
- locale_code: ru-RU
- - file: mobile/assets/i18n/zh-CN.json
- locale_code: zh-CN
- - file: mobile/assets/i18n/sk-SK.json
- locale_code: sk-SK
- - file: mobile/assets/i18n/nl-NL.json
- locale_code: nl-NL
- - file: mobile/assets/i18n/nb-NO.json
- locale_code: nb-NO
- - file: mobile/assets/i18n/sv-SE.json
- locale_code: sv-SE
- - file: mobile/assets/i18n/mn.json
- locale_code: mn
- - file: mobile/assets/i18n/ko-KR.json
- locale_code: ko-KR
- - file: mobile/assets/i18n/sr-Latn.json
- locale_code: sr-Latn
- - file: mobile/assets/i18n/sr-Cyrl.json
- locale_code: sr-Cyrl
- - file: mobile/assets/i18n/hi-IN.json
- locale_code: hi-IN
- - file: mobile/assets/i18n/es-PE.json
- locale_code: es-PE
- - file: mobile/assets/i18n/es-MX.json
- locale_code: es-MX
- - file: mobile/assets/i18n/sv-FI.json
- locale_code: sv-FI
- - file: mobile/assets/i18n/ca.json
- locale_code: ca
- - file: mobile/assets/i18n/hu-HU.json
- locale_code: hu-HU
- - file: mobile/assets/i18n/lv-LV.json
- locale_code: lv-LV
- - file: mobile/assets/i18n/zh-Hans.json
- locale_code: zh-Hans
- - file: mobile/assets/i18n/th-TH.json
- locale_code: th-TH
+config_version: 1.0
+project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
+file_type: json
+branch: main
+upload:
+ files:
+ - file: mobile/assets/i18n/en-US.json
+ locale_code: en-US
+download:
+ params:
+ export_empty_as: main
+ files:
+ - file: mobile/assets/i18n/en-US.json
+ locale_code: en-US
+ - file: mobile/assets/i18n/de-DE.json
+ locale_code: de-DE
+ - file: mobile/assets/i18n/da-DK.json
+ locale_code: da-DK
+ - file: mobile/assets/i18n/it-IT.json
+ locale_code: it-IT
+ - file: mobile/assets/i18n/es-ES.json
+ locale_code: es-ES
+ - file: mobile/assets/i18n/vi-VN.json
+ locale_code: vi-VN
+ - file: mobile/assets/i18n/fr-FR.json
+ locale_code: fr-FR
+ - file: mobile/assets/i18n/ja-JP.json
+ locale_code: ja-JP
+ - file: mobile/assets/i18n/pl-PL.json
+ locale_code: pl-PL
+ - file: mobile/assets/i18n/fi-FI.json
+ locale_code: fi-FI
+ - file: mobile/assets/i18n/pt-PT.json
+ locale_code: pt-PT
+ - file: mobile/assets/i18n/pt-BR.json
+ locale_code: pt-BR
+ - file: mobile/assets/i18n/cs-CZ.json
+ locale_code: cs-CZ
+ - file: mobile/assets/i18n/uk-UA.json
+ locale_code: uk-UA
+ - file: mobile/assets/i18n/ru-RU.json
+ locale_code: ru-RU
+ - file: mobile/assets/i18n/zh-CN.json
+ locale_code: zh-CN
+ - file: mobile/assets/i18n/sk-SK.json
+ locale_code: sk-SK
+ - file: mobile/assets/i18n/nl-NL.json
+ locale_code: nl-NL
+ - file: mobile/assets/i18n/nb-NO.json
+ locale_code: nb-NO
+ - file: mobile/assets/i18n/sv-SE.json
+ locale_code: sv-SE
+ - file: mobile/assets/i18n/mn.json
+ locale_code: mn
+ - file: mobile/assets/i18n/ko-KR.json
+ locale_code: ko-KR
+ - file: mobile/assets/i18n/sr-Latn.json
+ locale_code: sr-Latn
+ - file: mobile/assets/i18n/sr-Cyrl.json
+ locale_code: sr-Cyrl
+ - file: mobile/assets/i18n/hi-IN.json
+ locale_code: hi-IN
+ - file: mobile/assets/i18n/es-PE.json
+ locale_code: es-PE
+ - file: mobile/assets/i18n/es-MX.json
+ locale_code: es-MX
+ - file: mobile/assets/i18n/sv-FI.json
+ locale_code: sv-FI
+ - file: mobile/assets/i18n/ca-CA.json
+ locale_code: ca-CA
+ - file: mobile/assets/i18n/hu-HU.json
+ locale_code: hu-HU
+ - file: mobile/assets/i18n/lv-LV.json
+ locale_code: lv-LV
+ - file: mobile/assets/i18n/zh-Hans.json
+ locale_code: zh-Hans
+ - file: mobile/assets/i18n/th-TH.json
+ locale_code: th-TH
+ - file: mobile/assets/i18n/lt-LT.json
+ locale_code: lt-LT
+ - file: mobile/assets/i18n/el-GR.json
+ locale_code: el-GR
+ - file: mobile/assets/i18n/fr-CA.json
+ locale_code: fr-CA
+ - file: mobile/assets/i18n/es-US.json
+ locale_code: es-US
+ - file: mobile/assets/i18n/sl-SI.json
+ locale_code: sl-SI
+ - file: mobile/assets/i18n/ar-JO.json
+ locale_code: ar-JO
+ - file: mobile/assets/i18n/he-IL.json
+ locale_code: he-IL
+ - file: mobile/assets/i18n/ro-RO.json
+ locale_code: ro-RO
diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index f9ac99edb3..4975f866fd 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -400,7 +400,9 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings",
+ "setting_languages_title": "Languages",
"settings_require_restart": "Please restart Immich to apply this setting",
+ "setting_languages_apply": "Apply",
"share_add": "Add",
"share_add_photos": "Add photos",
"share_add_title": "Add a title",
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index d2414c23ec..64b4ea5474 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -1,121 +1,124 @@
-
- BGTaskSchedulerPermittedIdentifiers
-
- app.alextran.immich.backgroundFetch
- app.alextran.immich.backgroundProcessing
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Immich
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleLocalizations
-
- en
- de
- da
- it
- es
- vi
- fr
- ja
- pl
- fi
- pt
- cs
- uk
- ru
- zh
- sk
- nl
- nb
- sv
- mn
- ko
- sr
- hi
- ca
- hu
- lv
- th
- sl
-
- CFBundleName
- immich_mobile
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 1.101.0
- CFBundleSignature
- ????
- CFBundleVersion
- 147
- FLTEnableImpeller
-
- ITSAppUsesNonExemptEncryption
-
- LSApplicationQueriesSchemes
-
- https
-
- LSRequiresIPhoneOS
-
- MGLMapboxMetricsEnabledSettingShownInApp
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- NSCameraUsageDescription
- We need to access the camera to let you take beautiful video using this app
- NSLocationWhenInUseUsageDescription
- Enable location setting to show position of assets on map
- NSMicrophoneUsageDescription
- We need to access the microphone to let you take beautiful video using this app
- NSPhotoLibraryAddUsageDescription
- We need to manage backup your photos album
- NSPhotoLibraryUsageDescription
- We need to manage backup your photos album
- UIApplicationSupportsIndirectInputEvents
-
- UIBackgroundModes
-
- fetch
- processing
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UIStatusBarHidden
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- io.flutter.embedded_views_preview
-
-
-
+
+ BGTaskSchedulerPermittedIdentifiers
+
+ app.alextran.immich.backgroundFetch
+ app.alextran.immich.backgroundProcessing
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Immich
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLocalizations
+
+ en
+ ar
+ ca
+ cs
+ da
+ de
+ es
+ fi
+ fr
+ he
+ hi
+ hu
+ it
+ ja
+ ko
+ lv
+ mn
+ nb
+ nl
+ pl
+ pt
+ ro
+ ru
+ sk
+ sl
+ sr
+ sv
+ th
+ uk
+ vi
+ zh
+
+ CFBundleName
+ immich_mobile
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.101.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 147
+ FLTEnableImpeller
+
+ ITSAppUsesNonExemptEncryption
+
+ LSApplicationQueriesSchemes
+
+ https
+
+ LSRequiresIPhoneOS
+
+ MGLMapboxMetricsEnabledSettingShownInApp
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSCameraUsageDescription
+ We need to access the camera to let you take beautiful video using this app
+ NSLocationWhenInUseUsageDescription
+ Enable location setting to show position of assets on map
+ NSMicrophoneUsageDescription
+ We need to access the microphone to let you take beautiful video using this app
+ NSPhotoLibraryAddUsageDescription
+ We need to manage backup your photos album
+ NSPhotoLibraryUsageDescription
+ We need to manage backup your photos album
+ UIApplicationSupportsIndirectInputEvents
+
+ UIBackgroundModes
+
+ fetch
+ processing
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ io.flutter.embedded_views_preview
+
+
+
\ No newline at end of file
diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart
index 18df612dc4..e697e41c07 100644
--- a/mobile/lib/constants/locales.dart
+++ b/mobile/lib/constants/locales.dart
@@ -1,43 +1,48 @@
import 'dart:ui';
-const List locales = [
+const Map locales = {
// Default locale
- Locale('en', 'US'),
+ 'English (en_US)': Locale('en', 'US'),
// Additional locales
- Locale('de', 'DE'),
- Locale('da', 'DK'),
- Locale('it', 'IT'),
- Locale('es', 'ES'),
- Locale('vi', 'VN'),
- Locale('fr', 'CA'),
- Locale('fr', 'FR'),
- Locale('ja', 'JP'),
- Locale('pl', 'PL'),
- Locale('fi', 'FI'),
- Locale('pt', 'PT'),
- Locale('cs', 'CZ'),
- Locale('uk', 'UA'),
- Locale('ru', 'RU'),
- Locale('zh', 'CN'),
- Locale('sk', 'SK'),
- Locale('nl', 'NL'),
- Locale('nb', 'NO'),
- Locale('sv', 'SE'),
- Locale('mn', 'MN'),
- Locale('ko', 'KR'),
- Locale('sr', 'Latn'),
- Locale('sr', 'Cyrl'),
- Locale('hi', 'IN'),
- Locale('es', 'PE'),
- Locale('es', 'MX'),
- Locale('es', 'US'),
- Locale('sv', 'FI'),
- Locale('ca', 'CA'),
- Locale('hu', 'HU'),
- Locale('lv', 'LV'),
- Locale('zh', 'Hans'),
- Locale('th', 'TH'),
- Locale('sl', 'SI'),
-];
+ 'Arabic (ar_JO)': Locale('ar', 'JO'),
+ 'Catalan (ca_CA)': Locale('ca', 'CA'),
+ 'Chinese (zh_CN)': Locale('zh', 'CN'),
+ 'Chinese Simplified (zh_Hans)': Locale('zh', 'Hans'),
+ 'Czech (cs_CZ)': Locale('cs', 'CZ'),
+ 'Danish (da_DK)': Locale('da', 'DK'),
+ 'Dutch (nl_NL)': Locale('nl', 'NL'),
+ 'Finnish (fi_FI)': Locale('fi', 'FI'),
+ 'French (fr_CA)': Locale('fr', 'CA'),
+ 'French (fr_FR)': Locale('fr', 'FR'),
+ 'German (de_DE)': Locale('de', 'DE'),
+ 'Greek (el_GR)': Locale('el', 'GR'),
+ 'Hebrew (he_IL)': Locale('he', 'IL'),
+ 'Hindi (hi_IN)': Locale('hi', 'IN'),
+ 'Hungarian (hu_HU)': Locale('hu', 'HU'),
+ 'Italian (it_IT)': Locale('it', 'IT'),
+ 'Japanese (ja_JP)': Locale('ja', 'JP'),
+ 'Korean (ko_KR)': Locale('ko', 'KR'),
+ 'Latvian (lv_LV)': Locale('lv', 'LV'),
+ 'Lithuanian (lt_LT)': Locale('lt', 'LT'),
+ 'Mongolian (mn_MN)': Locale('mn', 'MN'),
+ 'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'),
+ 'Polish (pl_PL)': Locale('pl', 'PL'),
+ 'Portuguese (pt_PT)': Locale('pt', 'PT'),
+ 'Romanian (ro_RO)': Locale('ro', 'RO'),
+ 'Russian (ru_RU)': Locale('ru', 'RU'),
+ 'Serbian Cyrillic (sr_Cyrl)': Locale('sr', 'Cyrl'),
+ 'Serbian Latin (sr_Latn)': Locale('sr', 'Latn'),
+ 'Slovak (sk_SK)': Locale('sk', 'SK'),
+ 'Slovenian (sl_SI)': Locale('sl', 'SI'),
+ 'Spanish (es_ES)': Locale('es', 'ES'),
+ 'Spanish (es_MX)': Locale('es', 'MX'),
+ 'Spanish (es_PE)': Locale('es', 'PE'),
+ 'Spanish (es_US)': Locale('es', 'US'),
+ 'Swedish (sv_FI)': Locale('sv', 'FI'),
+ 'Swedish (sv_SE)': Locale('sv', 'SE'),
+ 'Thai (th_TH)': Locale('th', 'TH'),
+ 'Ukrainian (uk_UA)': Locale('uk', 'UA'),
+ 'Vietnamese (vi_VN)': Locale('vi', 'VN'),
+};
const String translationsPath = 'assets/i18n';
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index f20cf7ecc6..48cac8f7d1 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -1,225 +1,225 @@
-import 'dart:async';
-import 'dart:io';
-
-import 'package:device_info_plus/device_info_plus.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_displaymode/flutter_displaymode.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:timezone/data/latest.dart';
-import 'package:immich_mobile/constants/locales.dart';
-import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
-import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
-import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/routing/tab_navigation_observer.dart';
-import 'package:immich_mobile/shared/cache/widgets_binding.dart';
-import 'package:immich_mobile/shared/models/album.dart';
-import 'package:immich_mobile/shared/models/android_device_asset.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/models/etag.dart';
-import 'package:immich_mobile/shared/models/exif_info.dart';
-import 'package:immich_mobile/shared/models/ios_device_asset.dart';
-import 'package:immich_mobile/shared/models/logger_message.model.dart';
-import 'package:immich_mobile/shared/models/store.dart';
-import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/providers/app_state.provider.dart';
-import 'package:immich_mobile/shared/providers/db.provider.dart';
-import 'package:immich_mobile/shared/services/immich_logger.service.dart';
-import 'package:immich_mobile/shared/services/local_notification.service.dart';
-import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
-import 'package:immich_mobile/utils/migration.dart';
-import 'package:isar/isar.dart';
-import 'package:logging/logging.dart';
-import 'package:path_provider/path_provider.dart';
-
-void main() async {
- ImmichWidgetsBinding();
-
- final db = await loadDb();
- await initApp();
- await migrateDatabaseIfNeeded(db);
- HttpOverrides.global = HttpSSLCertOverride();
-
- runApp(
- ProviderScope(
- overrides: [dbProvider.overrideWithValue(db)],
- child: const MainWidget(),
- ),
- );
-}
-
-Future initApp() async {
- await EasyLocalization.ensureInitialized();
-
- if (kReleaseMode && Platform.isAndroid) {
- try {
- await FlutterDisplayMode.setHighRefreshRate();
- debugPrint("Enabled high refresh mode");
- } catch (e) {
- debugPrint("Error setting high refresh rate: $e");
- }
- }
-
- // Initialize Immich Logger Service
- ImmichLogger();
-
- var log = Logger("ImmichErrorLogger");
-
- FlutterError.onError = (details) {
- FlutterError.presentError(details);
- log.severe(
- 'FlutterError - Catch all',
- "${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}",
- details.stack,
- );
- };
-
- PlatformDispatcher.instance.onError = (error, stack) {
- log.severe('PlatformDispatcher - Catch all', error, stack);
- return true;
- };
-
- initializeTimeZones();
-}
-
-Future loadDb() async {
- final dir = await getApplicationDocumentsDirectory();
- Isar db = await Isar.open(
- [
- StoreValueSchema,
- ExifInfoSchema,
- AssetSchema,
- AlbumSchema,
- UserSchema,
- BackupAlbumSchema,
- DuplicatedAssetSchema,
- LoggerMessageSchema,
- ETagSchema,
- if (Platform.isAndroid) AndroidDeviceAssetSchema,
- if (Platform.isIOS) IOSDeviceAssetSchema,
- ],
- directory: dir.path,
- maxSizeMiB: 256,
- );
- Store.init(db);
- return db;
-}
-
-class ImmichApp extends ConsumerStatefulWidget {
- const ImmichApp({super.key});
-
- @override
- ImmichAppState createState() => ImmichAppState();
-}
-
-class ImmichAppState extends ConsumerState
- with WidgetsBindingObserver {
- @override
- void didChangeAppLifecycleState(AppLifecycleState state) {
- switch (state) {
- case AppLifecycleState.resumed:
- debugPrint("[APP STATE] resumed");
- ref.read(appStateProvider.notifier).handleAppResume();
- break;
- case AppLifecycleState.inactive:
- debugPrint("[APP STATE] inactive");
- ref.read(appStateProvider.notifier).handleAppInactivity();
- break;
- case AppLifecycleState.paused:
- debugPrint("[APP STATE] paused");
- ref.read(appStateProvider.notifier).handleAppPause();
- break;
- case AppLifecycleState.detached:
- debugPrint("[APP STATE] detached");
- ref.read(appStateProvider.notifier).handleAppDetached();
- break;
- case AppLifecycleState.hidden:
- debugPrint("[APP STATE] hidden");
- ref.read(appStateProvider.notifier).handleAppHidden();
- break;
- }
- }
-
- Future initApp() async {
- WidgetsBinding.instance.addObserver(this);
-
- // Draw the app from edge to edge
- SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
-
- // Sets the navigation bar color
- SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(
- systemNavigationBarColor: Colors.transparent,
- );
- if (Platform.isAndroid) {
- // Android 8 does not support transparent app bars
- final info = await DeviceInfoPlugin().androidInfo;
- if (info.version.sdkInt <= 26) {
- overlayStyle = context.isDarkTheme
- ? SystemUiOverlayStyle.dark
- : SystemUiOverlayStyle.light;
- }
- }
- SystemChrome.setSystemUIOverlayStyle(overlayStyle);
- await ref.read(localNotificationService).setup();
- }
-
- @override
- initState() {
- super.initState();
- initApp().then((_) => debugPrint("App Init Completed"));
- WidgetsBinding.instance.addPostFrameCallback((_) {
- // needs to be delayed so that EasyLocalization is working
- ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
- });
- }
-
- @override
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- var router = ref.watch(appRouterProvider);
-
- return MaterialApp(
- localizationsDelegates: context.localizationDelegates,
- supportedLocales: context.supportedLocales,
- locale: context.locale,
- debugShowCheckedModeBanner: false,
- home: MaterialApp.router(
- title: 'Immich',
- debugShowCheckedModeBanner: false,
- themeMode: ref.watch(immichThemeProvider),
- darkTheme: immichDarkTheme,
- theme: immichLightTheme,
- routeInformationParser: router.defaultRouteParser(),
- routerDelegate: router.delegate(
- navigatorObservers: () => [TabNavigationObserver(ref: ref)],
- ),
- ),
- );
- }
-}
-
-// ignore: prefer-single-widget-per-file
-class MainWidget extends StatelessWidget {
- const MainWidget({super.key});
-
- @override
- Widget build(BuildContext context) {
- return EasyLocalization(
- supportedLocales: locales,
- path: translationsPath,
- useFallbackTranslations: true,
- fallbackLocale: locales.first,
- child: const ImmichApp(),
- );
- }
-}
+import 'dart:async';
+import 'dart:io';
+
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_displaymode/flutter_displaymode.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:timezone/data/latest.dart';
+import 'package:immich_mobile/constants/locales.dart';
+import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
+import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
+import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/routing/tab_navigation_observer.dart';
+import 'package:immich_mobile/shared/cache/widgets_binding.dart';
+import 'package:immich_mobile/shared/models/album.dart';
+import 'package:immich_mobile/shared/models/android_device_asset.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/models/etag.dart';
+import 'package:immich_mobile/shared/models/exif_info.dart';
+import 'package:immich_mobile/shared/models/ios_device_asset.dart';
+import 'package:immich_mobile/shared/models/logger_message.model.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/models/user.dart';
+import 'package:immich_mobile/shared/providers/app_state.provider.dart';
+import 'package:immich_mobile/shared/providers/db.provider.dart';
+import 'package:immich_mobile/shared/services/immich_logger.service.dart';
+import 'package:immich_mobile/shared/services/local_notification.service.dart';
+import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
+import 'package:immich_mobile/utils/immich_app_theme.dart';
+import 'package:immich_mobile/utils/migration.dart';
+import 'package:isar/isar.dart';
+import 'package:logging/logging.dart';
+import 'package:path_provider/path_provider.dart';
+
+void main() async {
+ ImmichWidgetsBinding();
+
+ final db = await loadDb();
+ await initApp();
+ await migrateDatabaseIfNeeded(db);
+ HttpOverrides.global = HttpSSLCertOverride();
+
+ runApp(
+ ProviderScope(
+ overrides: [dbProvider.overrideWithValue(db)],
+ child: const MainWidget(),
+ ),
+ );
+}
+
+Future initApp() async {
+ await EasyLocalization.ensureInitialized();
+
+ if (kReleaseMode && Platform.isAndroid) {
+ try {
+ await FlutterDisplayMode.setHighRefreshRate();
+ debugPrint("Enabled high refresh mode");
+ } catch (e) {
+ debugPrint("Error setting high refresh rate: $e");
+ }
+ }
+
+ // Initialize Immich Logger Service
+ ImmichLogger();
+
+ var log = Logger("ImmichErrorLogger");
+
+ FlutterError.onError = (details) {
+ FlutterError.presentError(details);
+ log.severe(
+ 'FlutterError - Catch all',
+ "${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}",
+ details.stack,
+ );
+ };
+
+ PlatformDispatcher.instance.onError = (error, stack) {
+ log.severe('PlatformDispatcher - Catch all', error, stack);
+ return true;
+ };
+
+ initializeTimeZones();
+}
+
+Future loadDb() async {
+ final dir = await getApplicationDocumentsDirectory();
+ Isar db = await Isar.open(
+ [
+ StoreValueSchema,
+ ExifInfoSchema,
+ AssetSchema,
+ AlbumSchema,
+ UserSchema,
+ BackupAlbumSchema,
+ DuplicatedAssetSchema,
+ LoggerMessageSchema,
+ ETagSchema,
+ if (Platform.isAndroid) AndroidDeviceAssetSchema,
+ if (Platform.isIOS) IOSDeviceAssetSchema,
+ ],
+ directory: dir.path,
+ maxSizeMiB: 256,
+ );
+ Store.init(db);
+ return db;
+}
+
+class ImmichApp extends ConsumerStatefulWidget {
+ const ImmichApp({super.key});
+
+ @override
+ ImmichAppState createState() => ImmichAppState();
+}
+
+class ImmichAppState extends ConsumerState
+ with WidgetsBindingObserver {
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ switch (state) {
+ case AppLifecycleState.resumed:
+ debugPrint("[APP STATE] resumed");
+ ref.read(appStateProvider.notifier).handleAppResume();
+ break;
+ case AppLifecycleState.inactive:
+ debugPrint("[APP STATE] inactive");
+ ref.read(appStateProvider.notifier).handleAppInactivity();
+ break;
+ case AppLifecycleState.paused:
+ debugPrint("[APP STATE] paused");
+ ref.read(appStateProvider.notifier).handleAppPause();
+ break;
+ case AppLifecycleState.detached:
+ debugPrint("[APP STATE] detached");
+ ref.read(appStateProvider.notifier).handleAppDetached();
+ break;
+ case AppLifecycleState.hidden:
+ debugPrint("[APP STATE] hidden");
+ ref.read(appStateProvider.notifier).handleAppHidden();
+ break;
+ }
+ }
+
+ Future initApp() async {
+ WidgetsBinding.instance.addObserver(this);
+
+ // Draw the app from edge to edge
+ SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
+
+ // Sets the navigation bar color
+ SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(
+ systemNavigationBarColor: Colors.transparent,
+ );
+ if (Platform.isAndroid) {
+ // Android 8 does not support transparent app bars
+ final info = await DeviceInfoPlugin().androidInfo;
+ if (info.version.sdkInt <= 26) {
+ overlayStyle = context.isDarkTheme
+ ? SystemUiOverlayStyle.dark
+ : SystemUiOverlayStyle.light;
+ }
+ }
+ SystemChrome.setSystemUIOverlayStyle(overlayStyle);
+ await ref.read(localNotificationService).setup();
+ }
+
+ @override
+ initState() {
+ super.initState();
+ initApp().then((_) => debugPrint("App Init Completed"));
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ // needs to be delayed so that EasyLocalization is working
+ ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
+ });
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var router = ref.watch(appRouterProvider);
+
+ return MaterialApp(
+ localizationsDelegates: context.localizationDelegates,
+ supportedLocales: context.supportedLocales,
+ locale: context.locale,
+ debugShowCheckedModeBanner: false,
+ home: MaterialApp.router(
+ title: 'Immich',
+ debugShowCheckedModeBanner: false,
+ themeMode: ref.watch(immichThemeProvider),
+ darkTheme: immichDarkTheme,
+ theme: immichLightTheme,
+ routeInformationParser: router.defaultRouteParser(),
+ routerDelegate: router.delegate(
+ navigatorObservers: () => [TabNavigationObserver(ref: ref)],
+ ),
+ ),
+ );
+ }
+}
+
+// ignore: prefer-single-widget-per-file
+class MainWidget extends StatelessWidget {
+ const MainWidget({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return EasyLocalization(
+ supportedLocales: locales.values.toList(),
+ path: translationsPath,
+ useFallbackTranslations: true,
+ fallbackLocale: locales.values.first,
+ child: const ImmichApp(),
+ );
+ }
+}
diff --git a/mobile/lib/modules/backup/background_service/localization.dart b/mobile/lib/modules/backup/background_service/localization.dart
index a0c1610ece..c8ef662896 100644
--- a/mobile/lib/modules/backup/background_service/localization.dart
+++ b/mobile/lib/modules/backup/background_service/localization.dart
@@ -1,31 +1,31 @@
-// ignore_for_file: implementation_imports
-
-import 'package:flutter/foundation.dart';
-import 'package:easy_localization/src/asset_loader.dart';
-import 'package:easy_localization/src/easy_localization_controller.dart';
-import 'package:easy_localization/src/localization.dart';
-import 'package:immich_mobile/constants/locales.dart';
-
-/// Workaround to manually load translations in another Isolate
-Future loadTranslations() async {
- await EasyLocalizationController.initEasyLocation();
-
- final controller = EasyLocalizationController(
- supportedLocales: locales,
- useFallbackTranslations: true,
- saveLocale: true,
- assetLoader: const RootBundleAssetLoader(),
- path: translationsPath,
- useOnlyLangCode: false,
- onLoadError: (e) => debugPrint(e.toString()),
- fallbackLocale: locales.first,
- );
-
- await controller.loadTranslations();
-
- return Localization.load(
- controller.locale,
- translations: controller.translations,
- fallbackTranslations: controller.fallbackTranslations,
- );
-}
+// ignore_for_file: implementation_imports
+
+import 'package:flutter/foundation.dart';
+import 'package:easy_localization/src/asset_loader.dart';
+import 'package:easy_localization/src/easy_localization_controller.dart';
+import 'package:easy_localization/src/localization.dart';
+import 'package:immich_mobile/constants/locales.dart';
+
+/// Workaround to manually load translations in another Isolate
+Future loadTranslations() async {
+ await EasyLocalizationController.initEasyLocation();
+
+ final controller = EasyLocalizationController(
+ supportedLocales: locales.values.toList(),
+ useFallbackTranslations: true,
+ saveLocale: true,
+ assetLoader: const RootBundleAssetLoader(),
+ path: translationsPath,
+ useOnlyLangCode: false,
+ onLoadError: (e) => debugPrint(e.toString()),
+ fallbackLocale: locales.values.first,
+ );
+
+ await controller.loadTranslations();
+
+ return Localization.load(
+ controller.locale,
+ translations: controller.translations,
+ fallbackTranslations: controller.fallbackTranslations,
+ );
+}
diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart
index eeb4b379f2..bfe8899924 100644
--- a/mobile/lib/modules/settings/views/settings_page.dart
+++ b/mobile/lib/modules/settings/views/settings_page.dart
@@ -2,7 +2,10 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/modules/backup/background_service/localization.dart';
import 'package:immich_mobile/modules/settings/ui/advanced_settings.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart';
@@ -16,6 +19,7 @@ enum SettingSection {
'setting_notifications_title',
Icons.notifications_none_rounded,
),
+ languages('setting_languages_title', Icons.language),
preferences('preferences_settings_title', Icons.interests_outlined),
backup('backup_controller_page_backup', Icons.cloud_upload_outlined),
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined),
@@ -27,6 +31,7 @@ enum SettingSection {
Widget get widget => switch (this) {
SettingSection.notifications => const NotificationSetting(),
+ SettingSection.languages => const LanguageSettings(),
SettingSection.preferences => const PreferenceSetting(),
SettingSection.backup => const BackupSettings(),
SettingSection.timeline => const AssetListSettings(),
@@ -37,6 +42,70 @@ enum SettingSection {
const SettingSection(this.title, this.icon);
}
+class LanguageSettings extends HookConsumerWidget {
+ const LanguageSettings({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final currentLocale = context.locale;
+ final textController = useTextEditingController(
+ text: locales.keys.firstWhere(
+ (countryName) => locales[countryName] == currentLocale,
+ ),
+ );
+
+ final selectedLocale = useState(currentLocale);
+
+ return ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ DropdownMenu(
+ inputDecorationTheme: InputDecorationTheme(
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ contentPadding: const EdgeInsets.only(left: 16),
+ ),
+ menuStyle: MenuStyle(
+ shape: MaterialStatePropertyAll(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15),
+ ),
+ ),
+ ),
+ menuHeight: context.height * 0.5,
+ hintText: "Languages",
+ label: const Text('Languages'),
+ dropdownMenuEntries: locales.keys
+ .map(
+ (countryName) => DropdownMenuEntry(
+ value: locales[countryName],
+ label: countryName,
+ ),
+ )
+ .toList(),
+ controller: textController,
+ onSelected: (value) {
+ if (value != null) {
+ selectedLocale.value = value;
+ }
+ },
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: selectedLocale.value == currentLocale
+ ? null
+ : () {
+ context.setLocale(selectedLocale.value);
+ loadTranslations();
+ },
+ child: const Text('setting_languages_apply').tr(),
+ ),
+ ],
+ );
+ }
+}
+
@RoutePage()
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});