1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(mobile): in app language selector (#8574)

* feat(mobile): select locale in the mobile app

* add additional locale

* use the same locale variable across the app

* using different data structure

* drop down with button

* update pull locales

* open app ios

* remove dependency

* format fix
This commit is contained in:
Alex 2024-04-06 21:58:35 -05:00 committed by GitHub
parent 335c03d0b8
commit 82aeb3292a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 584 additions and 489 deletions

View file

@ -1,78 +1,94 @@
config_version: 1.0 config_version: 1.0
project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7 project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
file_type: json file_type: json
branch: main branch: main
upload: upload:
files: files:
- file: mobile/assets/i18n/en-US.json - file: mobile/assets/i18n/en-US.json
locale_code: en-US locale_code: en-US
download: download:
params: params:
export_empty_as: main export_empty_as: main
files: files:
- file: mobile/assets/i18n/en-US.json - file: mobile/assets/i18n/en-US.json
locale_code: en-US locale_code: en-US
- file: mobile/assets/i18n/de-DE.json - file: mobile/assets/i18n/de-DE.json
locale_code: de-DE locale_code: de-DE
- file: mobile/assets/i18n/da-DK.json - file: mobile/assets/i18n/da-DK.json
locale_code: da-DK locale_code: da-DK
- file: mobile/assets/i18n/it-IT.json - file: mobile/assets/i18n/it-IT.json
locale_code: it-IT locale_code: it-IT
- file: mobile/assets/i18n/es-ES.json - file: mobile/assets/i18n/es-ES.json
locale_code: es-ES locale_code: es-ES
- file: mobile/assets/i18n/vi-VN.json - file: mobile/assets/i18n/vi-VN.json
locale_code: vi-VN locale_code: vi-VN
- file: mobile/assets/i18n/fr-FR.json - file: mobile/assets/i18n/fr-FR.json
locale_code: fr-FR locale_code: fr-FR
- file: mobile/assets/i18n/ja-JP.json - file: mobile/assets/i18n/ja-JP.json
locale_code: ja-JP locale_code: ja-JP
- file: mobile/assets/i18n/pl-PL.json - file: mobile/assets/i18n/pl-PL.json
locale_code: pl-PL locale_code: pl-PL
- file: mobile/assets/i18n/fi-FI.json - file: mobile/assets/i18n/fi-FI.json
locale_code: fi-FI locale_code: fi-FI
- file: mobile/assets/i18n/pt-PT.json - file: mobile/assets/i18n/pt-PT.json
locale_code: pt-PT locale_code: pt-PT
- file: mobile/assets/i18n/pt-BR.json - file: mobile/assets/i18n/pt-BR.json
locale_code: pt-BR locale_code: pt-BR
- file: mobile/assets/i18n/cs-CZ.json - file: mobile/assets/i18n/cs-CZ.json
locale_code: cs-CZ locale_code: cs-CZ
- file: mobile/assets/i18n/uk-UA.json - file: mobile/assets/i18n/uk-UA.json
locale_code: uk-UA locale_code: uk-UA
- file: mobile/assets/i18n/ru-RU.json - file: mobile/assets/i18n/ru-RU.json
locale_code: ru-RU locale_code: ru-RU
- file: mobile/assets/i18n/zh-CN.json - file: mobile/assets/i18n/zh-CN.json
locale_code: zh-CN locale_code: zh-CN
- file: mobile/assets/i18n/sk-SK.json - file: mobile/assets/i18n/sk-SK.json
locale_code: sk-SK locale_code: sk-SK
- file: mobile/assets/i18n/nl-NL.json - file: mobile/assets/i18n/nl-NL.json
locale_code: nl-NL locale_code: nl-NL
- file: mobile/assets/i18n/nb-NO.json - file: mobile/assets/i18n/nb-NO.json
locale_code: nb-NO locale_code: nb-NO
- file: mobile/assets/i18n/sv-SE.json - file: mobile/assets/i18n/sv-SE.json
locale_code: sv-SE locale_code: sv-SE
- file: mobile/assets/i18n/mn.json - file: mobile/assets/i18n/mn.json
locale_code: mn locale_code: mn
- file: mobile/assets/i18n/ko-KR.json - file: mobile/assets/i18n/ko-KR.json
locale_code: ko-KR locale_code: ko-KR
- file: mobile/assets/i18n/sr-Latn.json - file: mobile/assets/i18n/sr-Latn.json
locale_code: sr-Latn locale_code: sr-Latn
- file: mobile/assets/i18n/sr-Cyrl.json - file: mobile/assets/i18n/sr-Cyrl.json
locale_code: sr-Cyrl locale_code: sr-Cyrl
- file: mobile/assets/i18n/hi-IN.json - file: mobile/assets/i18n/hi-IN.json
locale_code: hi-IN locale_code: hi-IN
- file: mobile/assets/i18n/es-PE.json - file: mobile/assets/i18n/es-PE.json
locale_code: es-PE locale_code: es-PE
- file: mobile/assets/i18n/es-MX.json - file: mobile/assets/i18n/es-MX.json
locale_code: es-MX locale_code: es-MX
- file: mobile/assets/i18n/sv-FI.json - file: mobile/assets/i18n/sv-FI.json
locale_code: sv-FI locale_code: sv-FI
- file: mobile/assets/i18n/ca.json - file: mobile/assets/i18n/ca-CA.json
locale_code: ca locale_code: ca-CA
- file: mobile/assets/i18n/hu-HU.json - file: mobile/assets/i18n/hu-HU.json
locale_code: hu-HU locale_code: hu-HU
- file: mobile/assets/i18n/lv-LV.json - file: mobile/assets/i18n/lv-LV.json
locale_code: lv-LV locale_code: lv-LV
- file: mobile/assets/i18n/zh-Hans.json - file: mobile/assets/i18n/zh-Hans.json
locale_code: zh-Hans locale_code: zh-Hans
- file: mobile/assets/i18n/th-TH.json - file: mobile/assets/i18n/th-TH.json
locale_code: th-TH 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

View file

@ -400,7 +400,9 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Settings",
"setting_languages_title": "Languages",
"settings_require_restart": "Please restart Immich to apply this setting", "settings_require_restart": "Please restart Immich to apply this setting",
"setting_languages_apply": "Apply",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",

View file

@ -1,121 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>app.alextran.immich.backgroundFetch</string> <string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string> <string>app.alextran.immich.backgroundProcessing</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true />
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Immich</string> <string>Immich</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>de</string> <string>ar</string>
<string>da</string> <string>ca</string>
<string>it</string> <string>cs</string>
<string>es</string> <string>da</string>
<string>vi</string> <string>de</string>
<string>fr</string> <string>es</string>
<string>ja</string> <string>fi</string>
<string>pl</string> <string>fr</string>
<string>fi</string> <string>he</string>
<string>pt</string> <string>hi</string>
<string>cs</string> <string>hu</string>
<string>uk</string> <string>it</string>
<string>ru</string> <string>ja</string>
<string>zh</string> <string>ko</string>
<string>sk</string> <string>lv</string>
<string>nl</string> <string>mn</string>
<string>nb</string> <string>nb</string>
<string>sv</string> <string>nl</string>
<string>mn</string> <string>pl</string>
<string>ko</string> <string>pt</string>
<string>sr</string> <string>ro</string>
<string>hi</string> <string>ru</string>
<string>ca</string> <string>sk</string>
<string>hu</string> <string>sl</string>
<string>lv</string> <string>sr</string>
<string>th</string> <string>sv</string>
<string>sl</string> <string>th</string>
</array> <string>uk</string>
<key>CFBundleName</key> <string>vi</string>
<string>immich_mobile</string> <string>zh</string>
<key>CFBundlePackageType</key> </array>
<string>APPL</string> <key>CFBundleName</key>
<key>CFBundleShortVersionString</key> <string>immich_mobile</string>
<string>1.101.0</string> <key>CFBundlePackageType</key>
<key>CFBundleSignature</key> <string>APPL</string>
<string>????</string> <key>CFBundleShortVersionString</key>
<key>CFBundleVersion</key> <string>1.101.0</string>
<string>147</string> <key>CFBundleSignature</key>
<key>FLTEnableImpeller</key> <string>????</string>
<true/> <key>CFBundleVersion</key>
<key>ITSAppUsesNonExemptEncryption</key> <string>147</string>
<false/> <key>FLTEnableImpeller</key>
<key>LSApplicationQueriesSchemes</key> <true />
<array> <key>ITSAppUsesNonExemptEncryption</key>
<string>https</string> <false />
</array> <key>LSApplicationQueriesSchemes</key>
<key>LSRequiresIPhoneOS</key> <array>
<true/> <string>https</string>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> </array>
<true/> <key>LSRequiresIPhoneOS</key>
<key>NSAppTransportSecurity</key> <true />
<dict> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<key>NSAllowsArbitraryLoads</key> <true />
<true/> <key>NSAppTransportSecurity</key>
</dict> <dict>
<key>NSCameraUsageDescription</key> <key>NSAllowsArbitraryLoads</key>
<string>We need to access the camera to let you take beautiful video using this app</string> <true />
<key>NSLocationWhenInUseUsageDescription</key> </dict>
<string>Enable location setting to show position of assets on map</string> <key>NSCameraUsageDescription</key>
<key>NSMicrophoneUsageDescription</key> <string>We need to access the camera to let you take beautiful video using this app</string>
<string>We need to access the microphone to let you take beautiful video using this app</string> <key>NSLocationWhenInUseUsageDescription</key>
<key>NSPhotoLibraryAddUsageDescription</key> <string>Enable location setting to show position of assets on map</string>
<string>We need to manage backup your photos album</string> <key>NSMicrophoneUsageDescription</key>
<key>NSPhotoLibraryUsageDescription</key> <string>We need to access the microphone to let you take beautiful video using this app</string>
<string>We need to manage backup your photos album</string> <key>NSPhotoLibraryAddUsageDescription</key>
<key>UIApplicationSupportsIndirectInputEvents</key> <string>We need to manage backup your photos album</string>
<true/> <key>NSPhotoLibraryUsageDescription</key>
<key>UIBackgroundModes</key> <string>We need to manage backup your photos album</string>
<array> <key>UIApplicationSupportsIndirectInputEvents</key>
<string>fetch</string> <true />
<string>processing</string> <key>UIBackgroundModes</key>
</array> <array>
<key>UILaunchStoryboardName</key> <string>fetch</string>
<string>LaunchScreen</string> <string>processing</string>
<key>UIMainStoryboardFile</key> </array>
<string>Main</string> <key>UILaunchStoryboardName</key>
<key>UIStatusBarHidden</key> <string>LaunchScreen</string>
<false/> <key>UIMainStoryboardFile</key>
<key>UISupportedInterfaceOrientations</key> <string>Main</string>
<array> <key>UIStatusBarHidden</key>
<string>UIInterfaceOrientationPortrait</string> <false />
<string>UIInterfaceOrientationLandscapeLeft</string> <key>UISupportedInterfaceOrientations</key>
<string>UIInterfaceOrientationLandscapeRight</string> <array>
</array> <string>UIInterfaceOrientationPortrait</string>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIInterfaceOrientationLandscapeLeft</string>
<array> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string> </array>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <key>UISupportedInterfaceOrientations~ipad</key>
<string>UIInterfaceOrientationLandscapeLeft</string> <array>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationPortrait</string>
</array> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <string>UIInterfaceOrientationLandscapeLeft</string>
<true/> <string>UIInterfaceOrientationLandscapeRight</string>
<key>io.flutter.embedded_views_preview</key> </array>
<true/> <key>UIViewControllerBasedStatusBarAppearance</key>
</dict> <true />
</plist> <key>io.flutter.embedded_views_preview</key>
<true />
</dict>
</plist>

View file

@ -1,43 +1,48 @@
import 'dart:ui'; import 'dart:ui';
const List<Locale> locales = [ const Map<String, Locale> locales = {
// Default locale // Default locale
Locale('en', 'US'), 'English (en_US)': Locale('en', 'US'),
// Additional locales // Additional locales
Locale('de', 'DE'), 'Arabic (ar_JO)': Locale('ar', 'JO'),
Locale('da', 'DK'), 'Catalan (ca_CA)': Locale('ca', 'CA'),
Locale('it', 'IT'), 'Chinese (zh_CN)': Locale('zh', 'CN'),
Locale('es', 'ES'), 'Chinese Simplified (zh_Hans)': Locale('zh', 'Hans'),
Locale('vi', 'VN'), 'Czech (cs_CZ)': Locale('cs', 'CZ'),
Locale('fr', 'CA'), 'Danish (da_DK)': Locale('da', 'DK'),
Locale('fr', 'FR'), 'Dutch (nl_NL)': Locale('nl', 'NL'),
Locale('ja', 'JP'), 'Finnish (fi_FI)': Locale('fi', 'FI'),
Locale('pl', 'PL'), 'French (fr_CA)': Locale('fr', 'CA'),
Locale('fi', 'FI'), 'French (fr_FR)': Locale('fr', 'FR'),
Locale('pt', 'PT'), 'German (de_DE)': Locale('de', 'DE'),
Locale('cs', 'CZ'), 'Greek (el_GR)': Locale('el', 'GR'),
Locale('uk', 'UA'), 'Hebrew (he_IL)': Locale('he', 'IL'),
Locale('ru', 'RU'), 'Hindi (hi_IN)': Locale('hi', 'IN'),
Locale('zh', 'CN'), 'Hungarian (hu_HU)': Locale('hu', 'HU'),
Locale('sk', 'SK'), 'Italian (it_IT)': Locale('it', 'IT'),
Locale('nl', 'NL'), 'Japanese (ja_JP)': Locale('ja', 'JP'),
Locale('nb', 'NO'), 'Korean (ko_KR)': Locale('ko', 'KR'),
Locale('sv', 'SE'), 'Latvian (lv_LV)': Locale('lv', 'LV'),
Locale('mn', 'MN'), 'Lithuanian (lt_LT)': Locale('lt', 'LT'),
Locale('ko', 'KR'), 'Mongolian (mn_MN)': Locale('mn', 'MN'),
Locale('sr', 'Latn'), 'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'),
Locale('sr', 'Cyrl'), 'Polish (pl_PL)': Locale('pl', 'PL'),
Locale('hi', 'IN'), 'Portuguese (pt_PT)': Locale('pt', 'PT'),
Locale('es', 'PE'), 'Romanian (ro_RO)': Locale('ro', 'RO'),
Locale('es', 'MX'), 'Russian (ru_RU)': Locale('ru', 'RU'),
Locale('es', 'US'), 'Serbian Cyrillic (sr_Cyrl)': Locale('sr', 'Cyrl'),
Locale('sv', 'FI'), 'Serbian Latin (sr_Latn)': Locale('sr', 'Latn'),
Locale('ca', 'CA'), 'Slovak (sk_SK)': Locale('sk', 'SK'),
Locale('hu', 'HU'), 'Slovenian (sl_SI)': Locale('sl', 'SI'),
Locale('lv', 'LV'), 'Spanish (es_ES)': Locale('es', 'ES'),
Locale('zh', 'Hans'), 'Spanish (es_MX)': Locale('es', 'MX'),
Locale('th', 'TH'), 'Spanish (es_PE)': Locale('es', 'PE'),
Locale('sl', 'SI'), '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'; const String translationsPath = 'assets/i18n';

View file

@ -1,225 +1,225 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:timezone/data/latest.dart'; import 'package:timezone/data/latest.dart';
import 'package:immich_mobile/constants/locales.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/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.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/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart';
import 'package:immich_mobile/shared/cache/widgets_binding.dart'; import 'package:immich_mobile/shared/cache/widgets_binding.dart';
import 'package:immich_mobile/shared/models/album.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/android_device_asset.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart'; import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.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/ios_device_asset.dart';
import 'package:immich_mobile/shared/models/logger_message.model.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/store.dart';
import 'package:immich_mobile/shared/models/user.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/app_state.provider.dart';
import 'package:immich_mobile/shared/providers/db.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/immich_logger.service.dart';
import 'package:immich_mobile/shared/services/local_notification.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/http_ssl_cert_override.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/migration.dart'; import 'package:immich_mobile/utils/migration.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
void main() async { void main() async {
ImmichWidgetsBinding(); ImmichWidgetsBinding();
final db = await loadDb(); final db = await loadDb();
await initApp(); await initApp();
await migrateDatabaseIfNeeded(db); await migrateDatabaseIfNeeded(db);
HttpOverrides.global = HttpSSLCertOverride(); HttpOverrides.global = HttpSSLCertOverride();
runApp( runApp(
ProviderScope( ProviderScope(
overrides: [dbProvider.overrideWithValue(db)], overrides: [dbProvider.overrideWithValue(db)],
child: const MainWidget(), child: const MainWidget(),
), ),
); );
} }
Future<void> initApp() async { Future<void> initApp() async {
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
if (kReleaseMode && Platform.isAndroid) { if (kReleaseMode && Platform.isAndroid) {
try { try {
await FlutterDisplayMode.setHighRefreshRate(); await FlutterDisplayMode.setHighRefreshRate();
debugPrint("Enabled high refresh mode"); debugPrint("Enabled high refresh mode");
} catch (e) { } catch (e) {
debugPrint("Error setting high refresh rate: $e"); debugPrint("Error setting high refresh rate: $e");
} }
} }
// Initialize Immich Logger Service // Initialize Immich Logger Service
ImmichLogger(); ImmichLogger();
var log = Logger("ImmichErrorLogger"); var log = Logger("ImmichErrorLogger");
FlutterError.onError = (details) { FlutterError.onError = (details) {
FlutterError.presentError(details); FlutterError.presentError(details);
log.severe( log.severe(
'FlutterError - Catch all', 'FlutterError - Catch all',
"${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}", "${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}",
details.stack, details.stack,
); );
}; };
PlatformDispatcher.instance.onError = (error, stack) { PlatformDispatcher.instance.onError = (error, stack) {
log.severe('PlatformDispatcher - Catch all', error, stack); log.severe('PlatformDispatcher - Catch all', error, stack);
return true; return true;
}; };
initializeTimeZones(); initializeTimeZones();
} }
Future<Isar> loadDb() async { Future<Isar> loadDb() async {
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
Isar db = await Isar.open( Isar db = await Isar.open(
[ [
StoreValueSchema, StoreValueSchema,
ExifInfoSchema, ExifInfoSchema,
AssetSchema, AssetSchema,
AlbumSchema, AlbumSchema,
UserSchema, UserSchema,
BackupAlbumSchema, BackupAlbumSchema,
DuplicatedAssetSchema, DuplicatedAssetSchema,
LoggerMessageSchema, LoggerMessageSchema,
ETagSchema, ETagSchema,
if (Platform.isAndroid) AndroidDeviceAssetSchema, if (Platform.isAndroid) AndroidDeviceAssetSchema,
if (Platform.isIOS) IOSDeviceAssetSchema, if (Platform.isIOS) IOSDeviceAssetSchema,
], ],
directory: dir.path, directory: dir.path,
maxSizeMiB: 256, maxSizeMiB: 256,
); );
Store.init(db); Store.init(db);
return db; return db;
} }
class ImmichApp extends ConsumerStatefulWidget { class ImmichApp extends ConsumerStatefulWidget {
const ImmichApp({super.key}); const ImmichApp({super.key});
@override @override
ImmichAppState createState() => ImmichAppState(); ImmichAppState createState() => ImmichAppState();
} }
class ImmichAppState extends ConsumerState<ImmichApp> class ImmichAppState extends ConsumerState<ImmichApp>
with WidgetsBindingObserver { with WidgetsBindingObserver {
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) { switch (state) {
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
debugPrint("[APP STATE] resumed"); debugPrint("[APP STATE] resumed");
ref.read(appStateProvider.notifier).handleAppResume(); ref.read(appStateProvider.notifier).handleAppResume();
break; break;
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive"); debugPrint("[APP STATE] inactive");
ref.read(appStateProvider.notifier).handleAppInactivity(); ref.read(appStateProvider.notifier).handleAppInactivity();
break; break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
debugPrint("[APP STATE] paused"); debugPrint("[APP STATE] paused");
ref.read(appStateProvider.notifier).handleAppPause(); ref.read(appStateProvider.notifier).handleAppPause();
break; break;
case AppLifecycleState.detached: case AppLifecycleState.detached:
debugPrint("[APP STATE] detached"); debugPrint("[APP STATE] detached");
ref.read(appStateProvider.notifier).handleAppDetached(); ref.read(appStateProvider.notifier).handleAppDetached();
break; break;
case AppLifecycleState.hidden: case AppLifecycleState.hidden:
debugPrint("[APP STATE] hidden"); debugPrint("[APP STATE] hidden");
ref.read(appStateProvider.notifier).handleAppHidden(); ref.read(appStateProvider.notifier).handleAppHidden();
break; break;
} }
} }
Future<void> initApp() async { Future<void> initApp() async {
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// Draw the app from edge to edge // Draw the app from edge to edge
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// Sets the navigation bar color // Sets the navigation bar color
SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle( SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
); );
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Android 8 does not support transparent app bars // Android 8 does not support transparent app bars
final info = await DeviceInfoPlugin().androidInfo; final info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt <= 26) { if (info.version.sdkInt <= 26) {
overlayStyle = context.isDarkTheme overlayStyle = context.isDarkTheme
? SystemUiOverlayStyle.dark ? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light; : SystemUiOverlayStyle.light;
} }
} }
SystemChrome.setSystemUIOverlayStyle(overlayStyle); SystemChrome.setSystemUIOverlayStyle(overlayStyle);
await ref.read(localNotificationService).setup(); await ref.read(localNotificationService).setup();
} }
@override @override
initState() { initState() {
super.initState(); super.initState();
initApp().then((_) => debugPrint("App Init Completed")); initApp().then((_) => debugPrint("App Init Completed"));
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working // needs to be delayed so that EasyLocalization is working
ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
}); });
} }
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var router = ref.watch(appRouterProvider); var router = ref.watch(appRouterProvider);
return MaterialApp( return MaterialApp(
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: MaterialApp.router( home: MaterialApp.router(
title: 'Immich', title: 'Immich',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
themeMode: ref.watch(immichThemeProvider), themeMode: ref.watch(immichThemeProvider),
darkTheme: immichDarkTheme, darkTheme: immichDarkTheme,
theme: immichLightTheme, theme: immichLightTheme,
routeInformationParser: router.defaultRouteParser(), routeInformationParser: router.defaultRouteParser(),
routerDelegate: router.delegate( routerDelegate: router.delegate(
navigatorObservers: () => [TabNavigationObserver(ref: ref)], navigatorObservers: () => [TabNavigationObserver(ref: ref)],
), ),
), ),
); );
} }
} }
// ignore: prefer-single-widget-per-file // ignore: prefer-single-widget-per-file
class MainWidget extends StatelessWidget { class MainWidget extends StatelessWidget {
const MainWidget({super.key}); const MainWidget({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EasyLocalization( return EasyLocalization(
supportedLocales: locales, supportedLocales: locales.values.toList(),
path: translationsPath, path: translationsPath,
useFallbackTranslations: true, useFallbackTranslations: true,
fallbackLocale: locales.first, fallbackLocale: locales.values.first,
child: const ImmichApp(), child: const ImmichApp(),
); );
} }
} }

View file

@ -1,31 +1,31 @@
// ignore_for_file: implementation_imports // ignore_for_file: implementation_imports
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:easy_localization/src/asset_loader.dart'; import 'package:easy_localization/src/asset_loader.dart';
import 'package:easy_localization/src/easy_localization_controller.dart'; import 'package:easy_localization/src/easy_localization_controller.dart';
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.dart';
/// Workaround to manually load translations in another Isolate /// Workaround to manually load translations in another Isolate
Future<bool> loadTranslations() async { Future<bool> loadTranslations() async {
await EasyLocalizationController.initEasyLocation(); await EasyLocalizationController.initEasyLocation();
final controller = EasyLocalizationController( final controller = EasyLocalizationController(
supportedLocales: locales, supportedLocales: locales.values.toList(),
useFallbackTranslations: true, useFallbackTranslations: true,
saveLocale: true, saveLocale: true,
assetLoader: const RootBundleAssetLoader(), assetLoader: const RootBundleAssetLoader(),
path: translationsPath, path: translationsPath,
useOnlyLangCode: false, useOnlyLangCode: false,
onLoadError: (e) => debugPrint(e.toString()), onLoadError: (e) => debugPrint(e.toString()),
fallbackLocale: locales.first, fallbackLocale: locales.values.first,
); );
await controller.loadTranslations(); await controller.loadTranslations();
return Localization.load( return Localization.load(
controller.locale, controller.locale,
translations: controller.translations, translations: controller.translations,
fallbackTranslations: controller.fallbackTranslations, fallbackTranslations: controller.fallbackTranslations,
); );
} }

View file

@ -2,7 +2,10 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/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/advanced_settings.dart';
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_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'; import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart';
@ -16,6 +19,7 @@ enum SettingSection {
'setting_notifications_title', 'setting_notifications_title',
Icons.notifications_none_rounded, Icons.notifications_none_rounded,
), ),
languages('setting_languages_title', Icons.language),
preferences('preferences_settings_title', Icons.interests_outlined), preferences('preferences_settings_title', Icons.interests_outlined),
backup('backup_controller_page_backup', Icons.cloud_upload_outlined), backup('backup_controller_page_backup', Icons.cloud_upload_outlined),
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined), timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined),
@ -27,6 +31,7 @@ enum SettingSection {
Widget get widget => switch (this) { Widget get widget => switch (this) {
SettingSection.notifications => const NotificationSetting(), SettingSection.notifications => const NotificationSetting(),
SettingSection.languages => const LanguageSettings(),
SettingSection.preferences => const PreferenceSetting(), SettingSection.preferences => const PreferenceSetting(),
SettingSection.backup => const BackupSettings(), SettingSection.backup => const BackupSettings(),
SettingSection.timeline => const AssetListSettings(), SettingSection.timeline => const AssetListSettings(),
@ -37,6 +42,70 @@ enum SettingSection {
const SettingSection(this.title, this.icon); 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<Locale>(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<OutlinedBorder>(
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() @RoutePage()
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});