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});