mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	Internationalization (German) of the mobile app. (#246)
* Add i18n framework to mobile app and write simple translation generator * Replace all texts in login_form with i18n keys * Localization of sharing section * Localization of asset viewer section * Use JSON as base translation format * Add check for missing/unused translation keys * Add localizely * Remove i18n directory in favour of localizely * Backup Translation * More translations * Translate home page * Translation of search page * Translate new server version announcement * Reformat code * Fix typo in german translation * Update englisch translations * Change translation keys to match dart filenames * Add /api to translated endpoint_urls * Update localizely.yml * Add languages to ios plist * Remove unused keys * Added script to check outdated key in other translations * Add download key to localizely.yml Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									f3032f74a4
								
							
						
					
					
						commit
						2b5cef156c
					
				
							
								
								
									
										15
									
								
								localizely.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								localizely.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
config_version: 1.0
 | 
			
		||||
project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
 | 
			
		||||
file_type: flutter_arb
 | 
			
		||||
upload:
 | 
			
		||||
  files:
 | 
			
		||||
    - file: mobile/assets/i18n/en-US.json
 | 
			
		||||
      locale_code: en
 | 
			
		||||
    - file: mobile/assets/i18n/de-DE.json
 | 
			
		||||
      locale_code: de
 | 
			
		||||
download:
 | 
			
		||||
  files:
 | 
			
		||||
    - file: mobile/assets/i18n/en-US.json
 | 
			
		||||
      locale_code: en
 | 
			
		||||
    - file: mobile/assets/i18n/de-DE.json
 | 
			
		||||
      locale_code: de
 | 
			
		||||
							
								
								
									
										98
									
								
								mobile/assets/i18n/de-DE.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								mobile/assets/i18n/de-DE.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
{
 | 
			
		||||
  "date_format": "E d. LLL y \u2022 hh:mm",
 | 
			
		||||
  "daily_title_text_date": "E, dd MMM",
 | 
			
		||||
  "daily_title_text_date_year": "E, dd MMM, yyyy",
 | 
			
		||||
  "monthly_title_text_date_format": "MMMM y",
 | 
			
		||||
  "login_form_button_text": "Anmelden",
 | 
			
		||||
  "login_form_save_login": "Angemeldet bleiben",
 | 
			
		||||
  "login_form_endpoint_url": "Server URL",
 | 
			
		||||
  "login_form_endpoint_hint": "http://deine-server-ip:port/api",
 | 
			
		||||
  "login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
 | 
			
		||||
  "login_form_err_leading_whitespace": "Führendes Leerzichen",
 | 
			
		||||
  "login_form_err_invalid_email": "Ungültige E-Mail",
 | 
			
		||||
  "login_form_err_http": "Bitte gebe http:// oder https:// an",
 | 
			
		||||
  "login_form_label_email": "E-Mail",
 | 
			
		||||
  "login_form_email_hint": "deine@email.de",
 | 
			
		||||
  "login_form_label_password": "Passwort",
 | 
			
		||||
  "login_form_password_hint": "password",
 | 
			
		||||
  "share_add_title": "Titel hinzufügen",
 | 
			
		||||
  "album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
 | 
			
		||||
  "album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
 | 
			
		||||
  "album_viewer_appbar_share_err_remove": "Beim Löschen von Elementen aus dem Album ist ein Problem aufgetreten",
 | 
			
		||||
  "album_viewer_appbar_share_err_title": "Der Titel konnte nicht geändert werden",
 | 
			
		||||
  "album_viewer_appbar_share_remove": "Entferne vom Album",
 | 
			
		||||
  "album_viewer_appbar_share_delete": "Album löschen",
 | 
			
		||||
  "album_viewer_appbar_share_leave": "Album verlassen",
 | 
			
		||||
  "sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
 | 
			
		||||
  "sharing_silver_appbar_share_partner": "Teile mit Partner",
 | 
			
		||||
  "share_add_photos": "Fotos hinzufügen",
 | 
			
		||||
  "album_viewer_page_share_add_users": "Nutzer hinzufügen",
 | 
			
		||||
  "share_add": "Hinzufügen",
 | 
			
		||||
  "create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
 | 
			
		||||
  "create_shared_album_page_share_select_photos": "Fotos auswählen",
 | 
			
		||||
  "share_create_album": "Album erstellen",
 | 
			
		||||
  "create_shared_album_page_share": "Teilen",
 | 
			
		||||
  "select_additional_user_for_sharing_page_suggestions": "Vorschläge",
 | 
			
		||||
  "share_invite": "Zum Album einladen",
 | 
			
		||||
  "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
 | 
			
		||||
  "sharing_page_empty_list": "LEERE LISTE",
 | 
			
		||||
  "sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
 | 
			
		||||
  "sharing_page_album": "Geteilte Alben",
 | 
			
		||||
  "exif_bottom_sheet_description": "Beschreibung hinzufügen...",
 | 
			
		||||
  "exif_bottom_sheet_location": "STANDORT",
 | 
			
		||||
  "exif_bottom_sheet_details": "DETAILS",
 | 
			
		||||
  "backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
 | 
			
		||||
  "backup_controller_page_server_storage": "Server Speicher",
 | 
			
		||||
  "backup_controller_page_status_on": "Sicherung ist aktiv",
 | 
			
		||||
  "backup_controller_page_status_off": "Sicherung ist inaktiv",
 | 
			
		||||
  "backup_controller_page_turn_off": "Sicherung ausschalten",
 | 
			
		||||
  "backup_controller_page_turn_on": "Sicherung einschalten",
 | 
			
		||||
  "backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
 | 
			
		||||
  "backup_controller_page_backup_selected": "Ausgewählt: ",
 | 
			
		||||
  "backup_all": "Alle",
 | 
			
		||||
  "backup_controller_page_none_selected": "Keine ausgewählt",
 | 
			
		||||
  "backup_controller_page_excluded": "Ausgeschlossen: ",
 | 
			
		||||
  "backup_controller_page_albums": "Gesicherte Alben",
 | 
			
		||||
  "backup_controller_page_to_backup": "Zu sichernde Alben",
 | 
			
		||||
  "backup_controller_page_select": "Auswählen",
 | 
			
		||||
  "backup_controller_page_backup": "Sicherung",
 | 
			
		||||
  "backup_controller_page_info": "Informationen zur Sicherung",
 | 
			
		||||
  "backup_controller_page_total": "Gesamt",
 | 
			
		||||
  "backup_controller_page_total_sub": "Alle Fotos und Videos",
 | 
			
		||||
  "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
 | 
			
		||||
  "backup_controller_page_remainder": "Übrig",
 | 
			
		||||
  "backup_controller_page_remainder_sub": "Noch zu sichernde Fotos und Videos",
 | 
			
		||||
  "backup_controller_page_cancel": "Abbrechen",
 | 
			
		||||
  "backup_controller_page_start_backup": "Sicherung starten",
 | 
			
		||||
  "album_info_card_backup_album_included": "EINGESCHLOSSEN",
 | 
			
		||||
  "album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
 | 
			
		||||
  "backup_info_card_assets": "Elemente",
 | 
			
		||||
  "backup_album_selection_page_select_albums": "Alben auswählen",
 | 
			
		||||
  "backup_album_selection_page_selection_info": "Auswahl",
 | 
			
		||||
  "backup_album_selection_page_total_assets": "Elemente",
 | 
			
		||||
  "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
 | 
			
		||||
  "backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
 | 
			
		||||
  "backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
 | 
			
		||||
  "backup_controller_page_storage_format": "{} von {} genutzt",
 | 
			
		||||
  "tab_controller_nav_photos": "Fotos",
 | 
			
		||||
  "tab_controller_nav_search": "Suche",
 | 
			
		||||
  "tab_controller_nav_sharing": "Teilen",
 | 
			
		||||
  "control_bottom_app_bar_delete": "Löschen",
 | 
			
		||||
  "delete_dialog_title": "Für immer löschen",
 | 
			
		||||
  "delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt",
 | 
			
		||||
  "delete_dialog_cancel": "Abbrechen",
 | 
			
		||||
  "delete_dialog_ok": "Löschen",
 | 
			
		||||
  "profile_drawer_sign_out": "Abmelden",
 | 
			
		||||
  "profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
 | 
			
		||||
  "search_bar_hint": "Durchsuche deine Fotos",
 | 
			
		||||
  "search_page_places": "Orte",
 | 
			
		||||
  "search_page_things": "Dinge",
 | 
			
		||||
  "search_result_page_new_search_hint": "Neue Suche",
 | 
			
		||||
  "search_page_no_places": "Keine Informationen über Orte verfügbar",
 | 
			
		||||
  "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
 | 
			
		||||
  "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
 | 
			
		||||
  "version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
 | 
			
		||||
  "version_announcement_overlay_release_notes": "Änderungsprotokoll",
 | 
			
		||||
  "version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
 | 
			
		||||
  "version_announcement_overlay_ack": "Ich habe verstanden"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										98
									
								
								mobile/assets/i18n/en-US.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								mobile/assets/i18n/en-US.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,98 @@
 | 
			
		||||
{
 | 
			
		||||
  "date_format": "E, LLL d, y \u2022 h:mm a",
 | 
			
		||||
  "daily_title_text_date": "E, MMM dd",
 | 
			
		||||
  "daily_title_text_date_year": "E, MMM dd, yyyy",
 | 
			
		||||
  "monthly_title_text_date_format": "MMMM y",
 | 
			
		||||
  "login_form_button_text": "Login",
 | 
			
		||||
  "login_form_save_login": "Stay logged in",
 | 
			
		||||
  "login_form_endpoint_url": "Server Endpoint URL",
 | 
			
		||||
  "login_form_endpoint_hint": "http://your-server-ip:port/api",
 | 
			
		||||
  "login_form_err_trailing_whitespace": "Trailing whitespace",
 | 
			
		||||
  "login_form_err_leading_whitespace": "Leading whitespace",
 | 
			
		||||
  "login_form_err_invalid_email": "Invalid Email",
 | 
			
		||||
  "login_form_err_http": "Please specify http:// or https://",
 | 
			
		||||
  "login_form_label_email": "Email",
 | 
			
		||||
  "login_form_email_hint": "youremail@email.com",
 | 
			
		||||
  "login_form_label_password": "Password",
 | 
			
		||||
  "login_form_password_hint": "password",
 | 
			
		||||
  "share_add_title": "Add a title",
 | 
			
		||||
  "album_viewer_appbar_share_err_delete": "Failed to delete album",
 | 
			
		||||
  "album_viewer_appbar_share_err_leave": "Failed to leave album",
 | 
			
		||||
  "album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
 | 
			
		||||
  "album_viewer_appbar_share_err_title": "Failed to change album title",
 | 
			
		||||
  "album_viewer_appbar_share_remove": "Remove from album",
 | 
			
		||||
  "album_viewer_appbar_share_delete": "Delete album",
 | 
			
		||||
  "album_viewer_appbar_share_leave": "Leave album",
 | 
			
		||||
  "sharing_silver_appbar_create_shared_album": "Create shared album",
 | 
			
		||||
  "sharing_silver_appbar_share_partner": "Share with partner",
 | 
			
		||||
  "share_add_photos": "Add photos",
 | 
			
		||||
  "album_viewer_page_share_add_users": "Add users",
 | 
			
		||||
  "share_add": "Add",
 | 
			
		||||
  "create_shared_album_page_share_add_assets": "ADD ASSETS",
 | 
			
		||||
  "create_shared_album_page_share_select_photos": "Select Photos",
 | 
			
		||||
  "share_create_album": "Create album",
 | 
			
		||||
  "create_shared_album_page_share": "Share",
 | 
			
		||||
  "select_additional_user_for_sharing_page_suggestions": "Suggestions",
 | 
			
		||||
  "share_invite": "Invite to album",
 | 
			
		||||
  "select_user_for_sharing_page_err_album": "Failed to create album",
 | 
			
		||||
  "sharing_page_empty_list": "EMPTY LIST",
 | 
			
		||||
  "sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
 | 
			
		||||
  "sharing_page_album": "Shared albums",
 | 
			
		||||
  "exif_bottom_sheet_description": "Add Description...",
 | 
			
		||||
  "exif_bottom_sheet_location": "LOCATION",
 | 
			
		||||
  "exif_bottom_sheet_details": "DETAILS",
 | 
			
		||||
  "backup_err_only_album": "Cannot remove the only album",
 | 
			
		||||
  "backup_controller_page_server_storage": "Server Storage",
 | 
			
		||||
  "backup_controller_page_status_on": "Backup is on",
 | 
			
		||||
  "backup_controller_page_status_off": "Backup is off",
 | 
			
		||||
  "backup_controller_page_turn_off": "Turn off Backup",
 | 
			
		||||
  "backup_controller_page_turn_on": "Turn on Backup",
 | 
			
		||||
  "backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
 | 
			
		||||
  "backup_controller_page_backup_selected": "Selected: ",
 | 
			
		||||
  "backup_all": "All",
 | 
			
		||||
  "backup_controller_page_none_selected": "None selected",
 | 
			
		||||
  "backup_controller_page_excluded": "Excluded: ",
 | 
			
		||||
  "backup_controller_page_albums": "Backup Albums",
 | 
			
		||||
  "backup_controller_page_to_backup": "Albums to be backup",
 | 
			
		||||
  "backup_controller_page_select": "Select",
 | 
			
		||||
  "backup_controller_page_backup": "Backup",
 | 
			
		||||
  "backup_controller_page_info": "Backup Information",
 | 
			
		||||
  "backup_controller_page_total": "Total",
 | 
			
		||||
  "backup_controller_page_total_sub": "All unique photos and videos from selected albums",
 | 
			
		||||
  "backup_controller_page_backup_sub": "Backed up photos and videos",
 | 
			
		||||
  "backup_controller_page_remainder": "Remainder",
 | 
			
		||||
  "backup_controller_page_remainder_sub": "Remaining photos and albums to back up from selection",
 | 
			
		||||
  "backup_controller_page_cancel": "Cancel",
 | 
			
		||||
  "backup_controller_page_start_backup": "Start Backup",
 | 
			
		||||
  "album_info_card_backup_album_included": "INCLUDED",
 | 
			
		||||
  "album_info_card_backup_album_excluded": "EXCLUDED",
 | 
			
		||||
  "backup_info_card_assets": "assets",
 | 
			
		||||
  "backup_album_selection_page_select_albums": "Select Albums",
 | 
			
		||||
  "backup_album_selection_page_selection_info": "Selection Info",
 | 
			
		||||
  "backup_album_selection_page_total_assets": "Total unique assets",
 | 
			
		||||
  "backup_album_selection_page_albums_device": "Albums on device ({})",
 | 
			
		||||
  "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
 | 
			
		||||
  "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
 | 
			
		||||
  "backup_controller_page_storage_format": "{} of {} used",
 | 
			
		||||
  "tab_controller_nav_photos": "Photos",
 | 
			
		||||
  "tab_controller_nav_search": "Search",
 | 
			
		||||
  "tab_controller_nav_sharing": "Sharing",
 | 
			
		||||
  "control_bottom_app_bar_delete": "Delete",
 | 
			
		||||
  "delete_dialog_title": "Delete Permanently",
 | 
			
		||||
  "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
 | 
			
		||||
  "delete_dialog_cancel": "Cancel",
 | 
			
		||||
  "delete_dialog_ok": "Delete",
 | 
			
		||||
  "profile_drawer_sign_out": "Sign Out",
 | 
			
		||||
  "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
 | 
			
		||||
  "search_bar_hint": "Search your photos",
 | 
			
		||||
  "search_page_places": "Places",
 | 
			
		||||
  "search_page_things": "Things",
 | 
			
		||||
  "search_result_page_new_search_hint": "New Search",
 | 
			
		||||
  "search_page_no_places": "No Places Info Available",
 | 
			
		||||
  "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
 | 
			
		||||
  "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
 | 
			
		||||
  "version_announcement_overlay_text_2": "please take your time to visit the ",
 | 
			
		||||
  "version_announcement_overlay_release_notes": "release notes",
 | 
			
		||||
  "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
 | 
			
		||||
  "version_announcement_overlay_ack": "Acknowledge"
 | 
			
		||||
}
 | 
			
		||||
@ -82,5 +82,11 @@
 | 
			
		||||
    <array>
 | 
			
		||||
      <string>https</string>
 | 
			
		||||
    </array>
 | 
			
		||||
 | 
			
		||||
    <key>CFBundleLocalizations</key>
 | 
			
		||||
    <array>
 | 
			
		||||
      <string>en</string>
 | 
			
		||||
      <string>de</string>
 | 
			
		||||
    </array>
 | 
			
		||||
  </dict>
 | 
			
		||||
</plist>
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
@ -36,7 +37,21 @@ void main() async {
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  runApp(const ProviderScope(child: ImmichApp()));
 | 
			
		||||
  await EasyLocalization.ensureInitialized();
 | 
			
		||||
 | 
			
		||||
  var locales = const [
 | 
			
		||||
    // Default locale
 | 
			
		||||
    Locale('en', 'US'),
 | 
			
		||||
    // Additional locales
 | 
			
		||||
    Locale('de', 'DE')
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  runApp(EasyLocalization(
 | 
			
		||||
      supportedLocales: locales,
 | 
			
		||||
      path: 'assets/i18n',
 | 
			
		||||
      useFallbackTranslations: true,
 | 
			
		||||
      fallbackLocale: locales.first,
 | 
			
		||||
      child: const ProviderScope(child: ImmichApp())));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ImmichApp extends ConsumerStatefulWidget {
 | 
			
		||||
@ -112,6 +127,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
 | 
			
		||||
    ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
 | 
			
		||||
 | 
			
		||||
    return MaterialApp(
 | 
			
		||||
      localizationsDelegates: context.localizationDelegates,
 | 
			
		||||
      supportedLocales: context.supportedLocales,
 | 
			
		||||
      locale: context.locale,
 | 
			
		||||
      debugShowCheckedModeBanner: false,
 | 
			
		||||
      home: Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_map/flutter_map.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
@ -72,7 +73,7 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
        children: [
 | 
			
		||||
          if (assetDetail.exifInfo?.dateTimeOriginal != null)
 | 
			
		||||
            Text(
 | 
			
		||||
              DateFormat('E, LLL d, y • h:mm a').format(
 | 
			
		||||
              DateFormat('date_format'.tr()).format(
 | 
			
		||||
                DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
 | 
			
		||||
              ),
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
@ -84,12 +85,12 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 16.0),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              "Add Description...",
 | 
			
		||||
              "exif_bottom_sheet_description",
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                color: Colors.grey[500],
 | 
			
		||||
                fontSize: 11,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
          // Location
 | 
			
		||||
@ -104,9 +105,9 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
                    color: Colors.grey[600],
 | 
			
		||||
                  ),
 | 
			
		||||
                  Text(
 | 
			
		||||
                    "LOCATION",
 | 
			
		||||
                    "exif_bottom_sheet_location",
 | 
			
		||||
                    style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
			
		||||
                  ),
 | 
			
		||||
                  ).tr(),
 | 
			
		||||
                  if (assetDetail.exifInfo?.latitude != null &&
 | 
			
		||||
                      assetDetail.exifInfo?.longitude != null)
 | 
			
		||||
                    _buildMap(),
 | 
			
		||||
@ -134,9 +135,9 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
                  Padding(
 | 
			
		||||
                    padding: const EdgeInsets.only(bottom: 8.0),
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      "DETAILS",
 | 
			
		||||
                      "exif_bottom_sheet_details",
 | 
			
		||||
                      style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ).tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  ListTile(
 | 
			
		||||
                    contentPadding: const EdgeInsets.all(0),
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import 'dart:typed_data';
 | 
			
		||||
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
			
		||||
@ -37,10 +38,10 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
			
		||||
          visualDensity: VisualDensity.compact,
 | 
			
		||||
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
			
		||||
          label: const Text(
 | 
			
		||||
            "INCLUDED",
 | 
			
		||||
            "album_info_card_backup_album_included",
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
                fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
			
		||||
          ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
          backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
        );
 | 
			
		||||
      } else if (isExcluded) {
 | 
			
		||||
@ -48,10 +49,10 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
			
		||||
          visualDensity: VisualDensity.compact,
 | 
			
		||||
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
			
		||||
          label: const Text(
 | 
			
		||||
            "EXCLUDED",
 | 
			
		||||
            "album_info_card_backup_album_excluded",
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
                fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
			
		||||
          ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
          backgroundColor: Colors.red[300],
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
@ -77,7 +78,7 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
			
		||||
          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
 | 
			
		||||
            ImmichToast.show(
 | 
			
		||||
              context: context,
 | 
			
		||||
              msg: "Cannot remove the only album",
 | 
			
		||||
              msg: "backup_err_only_album".tr(),
 | 
			
		||||
              toastType: ToastType.error,
 | 
			
		||||
              gravity: ToastGravity.BOTTOM,
 | 
			
		||||
            );
 | 
			
		||||
@ -104,7 +105,7 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
			
		||||
                  .contains(albumInfo)) {
 | 
			
		||||
            ImmichToast.show(
 | 
			
		||||
              context: context,
 | 
			
		||||
              msg: "Cannot exclude the only album",
 | 
			
		||||
              msg: "backup_err_only_album".tr(),
 | 
			
		||||
              toastType: ToastType.error,
 | 
			
		||||
              gravity: ToastGravity.BOTTOM,
 | 
			
		||||
            );
 | 
			
		||||
@ -180,7 +181,10 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
			
		||||
                          Padding(
 | 
			
		||||
                            padding: const EdgeInsets.only(top: 2.0),
 | 
			
		||||
                            child: Text(
 | 
			
		||||
                              '${albumInfo.assetCount} ${(albumInfo.isAll ? " (ALL)" : "")}',
 | 
			
		||||
                              albumInfo.assetCount.toString() +
 | 
			
		||||
                                  (albumInfo.isAll
 | 
			
		||||
                                      ? " (${'backup_all'.tr()})"
 | 
			
		||||
                                      : ""),
 | 
			
		||||
                              style: TextStyle(
 | 
			
		||||
                                  fontSize: 12, color: Colors.grey[600]),
 | 
			
		||||
                            ),
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
class BackupInfoCard extends StatelessWidget {
 | 
			
		||||
@ -44,7 +45,7 @@ class BackupInfoCard extends StatelessWidget {
 | 
			
		||||
              info,
 | 
			
		||||
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            const Text("assets"),
 | 
			
		||||
            const Text("backup_info_card_assets").tr(),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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:fluttertoast/fluttertoast.dart';
 | 
			
		||||
@ -55,7 +56,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
			
		||||
          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
 | 
			
		||||
            ImmichToast.show(
 | 
			
		||||
              context: context,
 | 
			
		||||
              msg: "Cannot remove the only album",
 | 
			
		||||
              msg: "backup_err_only_album".tr(),
 | 
			
		||||
              toastType: ToastType.error,
 | 
			
		||||
              gravity: ToastGravity.BOTTOM,
 | 
			
		||||
            );
 | 
			
		||||
@ -136,20 +137,21 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
			
		||||
          icon: const Icon(Icons.arrow_back_ios_rounded),
 | 
			
		||||
        ),
 | 
			
		||||
        title: const Text(
 | 
			
		||||
          "Select Albums",
 | 
			
		||||
          "backup_album_selection_page_select_albums",
 | 
			
		||||
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
 | 
			
		||||
        ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
      ),
 | 
			
		||||
      body: ListView(
 | 
			
		||||
        physics: const ClampingScrollPhysics(),
 | 
			
		||||
        children: [
 | 
			
		||||
          const Padding(
 | 
			
		||||
            padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              "Selection Info",
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding:
 | 
			
		||||
                const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
 | 
			
		||||
            child: const Text(
 | 
			
		||||
              "backup_album_selection_page_selection_info",
 | 
			
		||||
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
          ),
 | 
			
		||||
          // Selected Album Chips
 | 
			
		||||
 | 
			
		||||
@ -181,14 +183,18 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
			
		||||
                  ListTile(
 | 
			
		||||
                    visualDensity: VisualDensity.compact,
 | 
			
		||||
                    title: Text(
 | 
			
		||||
                      "Total unique assets",
 | 
			
		||||
                      "backup_album_selection_page_total_assets",
 | 
			
		||||
                      style: TextStyle(
 | 
			
		||||
                          fontWeight: FontWeight.bold,
 | 
			
		||||
                          fontSize: 14,
 | 
			
		||||
                          color: Colors.grey[700]),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ).tr(),
 | 
			
		||||
                    trailing: Text(
 | 
			
		||||
                      '${ref.watch(backupProvider).allUniqueAssets.length}',
 | 
			
		||||
                      ref
 | 
			
		||||
                          .watch(backupProvider)
 | 
			
		||||
                          .allUniqueAssets
 | 
			
		||||
                          .length
 | 
			
		||||
                          .toString(),
 | 
			
		||||
                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
@ -199,19 +205,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
          ListTile(
 | 
			
		||||
            title: Text(
 | 
			
		||||
              "Albums on device (${availableAlbums.length})",
 | 
			
		||||
              "backup_album_selection_page_albums_device"
 | 
			
		||||
                  .tr(args: [availableAlbums.length.toString()]),
 | 
			
		||||
              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
 | 
			
		||||
            ),
 | 
			
		||||
            subtitle: Padding(
 | 
			
		||||
              padding: const EdgeInsets.symmetric(vertical: 8.0),
 | 
			
		||||
              child: Text(
 | 
			
		||||
                "Tap to include, double tap to exclude",
 | 
			
		||||
                "backup_album_selection_page_albums_tap",
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  fontSize: 12,
 | 
			
		||||
                  color: Theme.of(context).primaryColor,
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              ).tr(),
 | 
			
		||||
            ),
 | 
			
		||||
            trailing: IconButton(
 | 
			
		||||
              splashRadius: 16,
 | 
			
		||||
@ -230,21 +237,21 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
			
		||||
                          borderRadius: BorderRadius.circular(12)),
 | 
			
		||||
                      elevation: 5,
 | 
			
		||||
                      title: Text(
 | 
			
		||||
                        'Selection Info',
 | 
			
		||||
                        'backup_album_selection_page_selection_info',
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                          fontSize: 16,
 | 
			
		||||
                          fontWeight: FontWeight.bold,
 | 
			
		||||
                          color: Theme.of(context).primaryColor,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      ).tr(),
 | 
			
		||||
                      content: SingleChildScrollView(
 | 
			
		||||
                        child: ListBody(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Text(
 | 
			
		||||
                              'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
 | 
			
		||||
                              'backup_album_selection_page_assets_scatter',
 | 
			
		||||
                              style: TextStyle(
 | 
			
		||||
                                  fontSize: 14, color: Colors.grey[700]),
 | 
			
		||||
                            ),
 | 
			
		||||
                            ).tr(),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -44,9 +45,9 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
          color: Theme.of(context).primaryColor,
 | 
			
		||||
        ),
 | 
			
		||||
        title: const Text(
 | 
			
		||||
          "Server storage",
 | 
			
		||||
          "backup_controller_page_server_storage",
 | 
			
		||||
          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
 | 
			
		||||
        ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        subtitle: Padding(
 | 
			
		||||
          padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
          child: Column(
 | 
			
		||||
@ -66,8 +67,11 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(top: 12.0),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                    '${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
 | 
			
		||||
                child: const Text('backup_controller_page_storage_format').tr(
 | 
			
		||||
                    args: [
 | 
			
		||||
                      backupState.serverInfo.diskUse,
 | 
			
		||||
                      backupState.serverInfo.diskSize
 | 
			
		||||
                    ]),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
@ -76,11 +80,13 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ListTile _buildBackupController() {
 | 
			
		||||
      var backUpOption =
 | 
			
		||||
          authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
 | 
			
		||||
      var backUpOption = authenticationState.deviceInfo.isAutoBackup
 | 
			
		||||
          ? "backup_controller_page_status_on".tr()
 | 
			
		||||
          : "backup_controller_page_status_off".tr();
 | 
			
		||||
      var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
 | 
			
		||||
      var backupBtnText =
 | 
			
		||||
          authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
 | 
			
		||||
      var backupBtnText = authenticationState.deviceInfo.isAutoBackup
 | 
			
		||||
          ? "backup_controller_page_turn_off".tr()
 | 
			
		||||
          : "backup_controller_page_turn_on".tr();
 | 
			
		||||
      return ListTile(
 | 
			
		||||
        isThreeLine: true,
 | 
			
		||||
        leading: isAutoBackup
 | 
			
		||||
@ -90,7 +96,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
              )
 | 
			
		||||
            : const Icon(Icons.cloud_off_rounded),
 | 
			
		||||
        title: Text(
 | 
			
		||||
          "Back up is $backUpOption",
 | 
			
		||||
          backUpOption,
 | 
			
		||||
          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
 | 
			
		||||
        ),
 | 
			
		||||
        subtitle: Padding(
 | 
			
		||||
@ -100,9 +106,9 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
            children: [
 | 
			
		||||
              if (!isAutoBackup)
 | 
			
		||||
                const Text(
 | 
			
		||||
                  "Turn on backup to automatically upload new assets to the server.",
 | 
			
		||||
                  "backup_controller_page_desc_backup",
 | 
			
		||||
                  style: TextStyle(fontSize: 14),
 | 
			
		||||
                ),
 | 
			
		||||
                ).tr(),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
                child: OutlinedButton(
 | 
			
		||||
@ -123,7 +129,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
                          .setAutoBackup(true);
 | 
			
		||||
                    }
 | 
			
		||||
                  },
 | 
			
		||||
                  child: Text("Turn $backupBtnText Backup",
 | 
			
		||||
                  child: Text(backupBtnText,
 | 
			
		||||
                      style: const TextStyle(fontWeight: FontWeight.bold)),
 | 
			
		||||
                ),
 | 
			
		||||
              )
 | 
			
		||||
@ -134,13 +140,13 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget _buildSelectedAlbumName() {
 | 
			
		||||
      var text = "Selected: ";
 | 
			
		||||
      var text = "backup_controller_page_backup_selected".tr();
 | 
			
		||||
      var albums = ref.watch(backupProvider).selectedBackupAlbums;
 | 
			
		||||
 | 
			
		||||
      if (albums.isNotEmpty) {
 | 
			
		||||
        for (var album in albums) {
 | 
			
		||||
          if (album.name == "Recent" || album.name == "Recents") {
 | 
			
		||||
            text += "${album.name} (All), ";
 | 
			
		||||
            text += "${album.name} (${'backup_all'.tr()}), ";
 | 
			
		||||
          } else {
 | 
			
		||||
            text += "${album.name}, ";
 | 
			
		||||
          }
 | 
			
		||||
@ -160,7 +166,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
        return Padding(
 | 
			
		||||
          padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            "None selected",
 | 
			
		||||
            "backup_controller_page_none_selected".tr(),
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
                color: Theme.of(context).primaryColor,
 | 
			
		||||
                fontSize: 12,
 | 
			
		||||
@ -171,7 +177,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget _buildExcludedAlbumName() {
 | 
			
		||||
      var text = "Excluded: ";
 | 
			
		||||
      var text = "backup_controller_page_excluded".tr();
 | 
			
		||||
      var albums = ref.watch(backupProvider).excludedBackupAlbums;
 | 
			
		||||
 | 
			
		||||
      if (albums.isNotEmpty) {
 | 
			
		||||
@ -207,17 +213,18 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
        borderOnForeground: false,
 | 
			
		||||
        child: ListTile(
 | 
			
		||||
          minVerticalPadding: 15,
 | 
			
		||||
          title: const Text("Backup Albums",
 | 
			
		||||
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
 | 
			
		||||
          title: const Text("backup_controller_page_albums",
 | 
			
		||||
                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20))
 | 
			
		||||
              .tr(),
 | 
			
		||||
          subtitle: Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
            child: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                const Text(
 | 
			
		||||
                  "Albums to be backed up",
 | 
			
		||||
                  "backup_controller_page_to_backup",
 | 
			
		||||
                  style: TextStyle(color: Color(0xFF808080), fontSize: 12),
 | 
			
		||||
                ),
 | 
			
		||||
                ).tr(),
 | 
			
		||||
                _buildSelectedAlbumName(),
 | 
			
		||||
                _buildExcludedAlbumName()
 | 
			
		||||
              ],
 | 
			
		||||
@ -234,14 +241,14 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
 | 
			
		||||
            },
 | 
			
		||||
            child: const Padding(
 | 
			
		||||
              padding: EdgeInsets.symmetric(
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: const EdgeInsets.symmetric(
 | 
			
		||||
                vertical: 16.0,
 | 
			
		||||
              ),
 | 
			
		||||
              child: Text(
 | 
			
		||||
                "Select",
 | 
			
		||||
              child: const Text(
 | 
			
		||||
                "backup_controller_page_select",
 | 
			
		||||
                style: TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
              ),
 | 
			
		||||
              ).tr(),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
@ -387,9 +394,9 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        title: const Text(
 | 
			
		||||
          "Backup",
 | 
			
		||||
          "backup_controller_page_backup",
 | 
			
		||||
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
 | 
			
		||||
        ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        leading: IconButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              ref.watch(websocketProvider.notifier).listenUploadEvent();
 | 
			
		||||
@ -405,27 +412,27 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
        child: ListView(
 | 
			
		||||
          // crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Padding(
 | 
			
		||||
              padding: EdgeInsets.all(8.0),
 | 
			
		||||
              child: Text(
 | 
			
		||||
                "Backup Information",
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.all(8.0),
 | 
			
		||||
              child: const Text(
 | 
			
		||||
                "backup_controller_page_info",
 | 
			
		||||
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
			
		||||
              ),
 | 
			
		||||
              ).tr(),
 | 
			
		||||
            ),
 | 
			
		||||
            _buildFolderSelectionTile(),
 | 
			
		||||
            BackupInfoCard(
 | 
			
		||||
              title: "Total",
 | 
			
		||||
              subtitle: "All unique photos and videos from selected albums",
 | 
			
		||||
              title: "backup_controller_page_total".tr(),
 | 
			
		||||
              subtitle: "backup_controller_page_total_sub".tr(),
 | 
			
		||||
              info: "${backupState.allUniqueAssets.length}",
 | 
			
		||||
            ),
 | 
			
		||||
            BackupInfoCard(
 | 
			
		||||
              title: "Backup",
 | 
			
		||||
              subtitle: "Backed up photos and videos",
 | 
			
		||||
              title: "backup_controller_page_backup".tr(),
 | 
			
		||||
              subtitle: "backup_controller_page_backup_sub".tr(),
 | 
			
		||||
              info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
			
		||||
            ),
 | 
			
		||||
            BackupInfoCard(
 | 
			
		||||
              title: "Remainder",
 | 
			
		||||
              subtitle: "Remaining photos and albums to back up from selection",
 | 
			
		||||
              title: "backup_controller_page_remainder".tr(),
 | 
			
		||||
              subtitle: "backup_controller_page_remainder_sub".tr(),
 | 
			
		||||
              info:
 | 
			
		||||
                  "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
			
		||||
            ),
 | 
			
		||||
@ -452,12 +459,12 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
                              ref.read(backupProvider.notifier).cancelBackup();
 | 
			
		||||
                            },
 | 
			
		||||
                            child: const Text(
 | 
			
		||||
                              "CANCEL",
 | 
			
		||||
                              "backup_controller_page_cancel",
 | 
			
		||||
                              style: TextStyle(
 | 
			
		||||
                                fontSize: 14,
 | 
			
		||||
                                fontWeight: FontWeight.bold,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            ).tr(),
 | 
			
		||||
                          )
 | 
			
		||||
                        : ElevatedButton(
 | 
			
		||||
                            style: ElevatedButton.styleFrom(
 | 
			
		||||
@ -467,12 +474,12 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
                            ),
 | 
			
		||||
                            onPressed: shouldBackup ? startBackup : null,
 | 
			
		||||
                            child: const Text(
 | 
			
		||||
                              "START BACKUP",
 | 
			
		||||
                              "backup_controller_page_start_backup",
 | 
			
		||||
                              style: TextStyle(
 | 
			
		||||
                                fontSize: 14,
 | 
			
		||||
                                fontWeight: FontWeight.bold,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            ).tr(),
 | 
			
		||||
                          ),
 | 
			
		||||
              ),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 | 
			
		||||
 | 
			
		||||
@ -26,7 +27,7 @@ class ControlBottomAppBar extends StatelessWidget {
 | 
			
		||||
                children: [
 | 
			
		||||
                  ControlBoxButton(
 | 
			
		||||
                    iconData: Icons.delete_forever_rounded,
 | 
			
		||||
                    label: "Delete",
 | 
			
		||||
                    label: "control_bottom_app_bar_delete".tr(),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      showDialog(
 | 
			
		||||
                        context: context,
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
@ -19,7 +20,7 @@ class DailyTitleText extends ConsumerWidget {
 | 
			
		||||
    var currentYear = DateTime.now().year;
 | 
			
		||||
    var groupYear = DateTime.parse(isoDate).year;
 | 
			
		||||
    var formatDateTemplate =
 | 
			
		||||
        currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
 | 
			
		||||
        currentYear == groupYear ? "daily_title_text_date".tr() : "daily_title_text_date_year".tr();
 | 
			
		||||
    var dateText =
 | 
			
		||||
        DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
@ -13,18 +14,17 @@ class DeleteDialog extends ConsumerWidget {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
      backgroundColor: Colors.grey[200],
 | 
			
		||||
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
 | 
			
		||||
      title: const Text("Delete Permanently"),
 | 
			
		||||
      content: const Text(
 | 
			
		||||
          "These items will be permanently deleted from Immich and from your device"),
 | 
			
		||||
      title: const Text("delete_dialog_title").tr(),
 | 
			
		||||
      content: const Text("delete_dialog_alert").tr(),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          },
 | 
			
		||||
          child: const Text(
 | 
			
		||||
            "Cancel",
 | 
			
		||||
            "delete_dialog_cancel",
 | 
			
		||||
            style: TextStyle(color: Colors.blueGrey),
 | 
			
		||||
          ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
@ -36,9 +36,9 @@ class DeleteDialog extends ConsumerWidget {
 | 
			
		||||
            Navigator.of(context).pop();
 | 
			
		||||
          },
 | 
			
		||||
          child: Text(
 | 
			
		||||
            "Delete",
 | 
			
		||||
            "delete_dialog_ok",
 | 
			
		||||
            style: TextStyle(color: Colors.red[400]),
 | 
			
		||||
          ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:intl/intl.dart';
 | 
			
		||||
 | 
			
		||||
@ -11,7 +12,7 @@ class MonthlyTitleText extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    var monthTitleText = DateFormat('MMMM y').format(DateTime.parse(isoDate));
 | 
			
		||||
    var monthTitleText = DateFormat("monthly_title_text_date_format".tr()).format(DateTime.parse(isoDate));
 | 
			
		||||
 | 
			
		||||
    return SliverToBoxAdapter(
 | 
			
		||||
      child: Padding(
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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:hive_flutter/hive_flutter.dart';
 | 
			
		||||
@ -183,12 +184,12 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                  color: Colors.black54,
 | 
			
		||||
                ),
 | 
			
		||||
                title: const Text(
 | 
			
		||||
                  "Sign Out",
 | 
			
		||||
                  "profile_drawer_sign_out",
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                      color: Colors.black54,
 | 
			
		||||
                      fontSize: 14,
 | 
			
		||||
                      fontWeight: FontWeight.bold),
 | 
			
		||||
                ),
 | 
			
		||||
                ).tr(),
 | 
			
		||||
                onTap: () async {
 | 
			
		||||
                  bool res =
 | 
			
		||||
                      await ref.watch(authenticationProvider.notifier).logout();
 | 
			
		||||
@ -227,7 +228,7 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        serverInfoState.isVersionMismatch
 | 
			
		||||
                            ? serverInfoState.versionMismatchErrorMessage
 | 
			
		||||
                            : "Client and Server are up-to-date",
 | 
			
		||||
                            : "profile_drawer_client_server_up_to_date".tr(),
 | 
			
		||||
                        textAlign: TextAlign.center,
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                            fontSize: 11,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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:hive/hive.dart';
 | 
			
		||||
@ -21,7 +22,7 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
    final passwordController =
 | 
			
		||||
        useTextEditingController.fromValue(TextEditingValue.empty);
 | 
			
		||||
    final serverEndpointController =
 | 
			
		||||
        useTextEditingController(text: 'http://your-server-ip:2283/api');
 | 
			
		||||
        useTextEditingController(text: 'login_endpoint_hint'.tr());
 | 
			
		||||
    final isSaveLoginInfo = useState<bool>(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
@ -73,12 +74,12 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
                    borderRadius: BorderRadius.circular(5)),
 | 
			
		||||
                enableFeedback: true,
 | 
			
		||||
                title: const Text(
 | 
			
		||||
                  "Stay logged in",
 | 
			
		||||
                  "login_form_save_login",
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                      fontSize: 16,
 | 
			
		||||
                      fontWeight: FontWeight.bold,
 | 
			
		||||
                      color: Colors.grey),
 | 
			
		||||
                ),
 | 
			
		||||
                ).tr(),
 | 
			
		||||
                value: isSaveLoginInfo.value,
 | 
			
		||||
                onChanged: (switchValue) {
 | 
			
		||||
                  if (switchValue != null) {
 | 
			
		||||
@ -111,7 +112,7 @@ class ServerEndpointInput extends StatelessWidget {
 | 
			
		||||
    if (url?.startsWith(RegExp(r'https?://')) == true) {
 | 
			
		||||
      return null;
 | 
			
		||||
    } else {
 | 
			
		||||
      return 'Please specify http:// or https://';
 | 
			
		||||
      return 'login_form_err_http'.tr();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -119,10 +120,10 @@ class ServerEndpointInput extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return TextFormField(
 | 
			
		||||
      controller: controller,
 | 
			
		||||
      decoration: const InputDecoration(
 | 
			
		||||
        labelText: 'Server Endpoint URL',
 | 
			
		||||
      decoration: InputDecoration(
 | 
			
		||||
        labelText: 'login_form_endpoint_url'.tr(),
 | 
			
		||||
        border: OutlineInputBorder(),
 | 
			
		||||
        hintText: 'http://your-server-ip:port',
 | 
			
		||||
        hintText: 'login_form_endpoint_hint'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      validator: _validateInput,
 | 
			
		||||
      autovalidateMode: AutovalidateMode.always,
 | 
			
		||||
@ -137,9 +138,10 @@ class EmailInput extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  String? _validateInput(String? email) {
 | 
			
		||||
    if (email == null || email == '') return null;
 | 
			
		||||
    if (email.endsWith(' ')) return 'Trailing whitespace';
 | 
			
		||||
    if (email.startsWith(' ')) return 'Leading whitespace';
 | 
			
		||||
    if (email.contains(' ') || !email.contains('@')) return 'Invalid Email';
 | 
			
		||||
    if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
 | 
			
		||||
    if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
 | 
			
		||||
    if (email.contains(' ') || !email.contains('@'))
 | 
			
		||||
      return 'login_form_err_invalid_email'.tr();
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -147,10 +149,10 @@ class EmailInput extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return TextFormField(
 | 
			
		||||
      controller: controller,
 | 
			
		||||
      decoration: const InputDecoration(
 | 
			
		||||
        labelText: 'Email',
 | 
			
		||||
      decoration: InputDecoration(
 | 
			
		||||
        labelText: 'login_form_label_email'.tr(),
 | 
			
		||||
        border: OutlineInputBorder(),
 | 
			
		||||
        hintText: 'youremail@email.com',
 | 
			
		||||
        hintText: 'login_form_email_hint'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      validator: _validateInput,
 | 
			
		||||
      autovalidateMode: AutovalidateMode.always,
 | 
			
		||||
@ -168,10 +170,10 @@ class PasswordInput extends StatelessWidget {
 | 
			
		||||
    return TextFormField(
 | 
			
		||||
      obscureText: true,
 | 
			
		||||
      controller: controller,
 | 
			
		||||
      decoration: const InputDecoration(
 | 
			
		||||
          labelText: 'Password',
 | 
			
		||||
      decoration: InputDecoration(
 | 
			
		||||
          labelText: 'login_form_label_password'.tr(),
 | 
			
		||||
          border: OutlineInputBorder(),
 | 
			
		||||
          hintText: 'password'),
 | 
			
		||||
          hintText: 'login_form_password_hint'.tr()),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -222,15 +224,14 @@ class LoginButton extends ConsumerWidget {
 | 
			
		||||
          } else {
 | 
			
		||||
            ImmichToast.show(
 | 
			
		||||
              context: context,
 | 
			
		||||
              msg:
 | 
			
		||||
                  "Error logging you in, check server url, email and password!",
 | 
			
		||||
              msg: "login_failed".tr(),
 | 
			
		||||
              toastType: ToastType.error,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        child: const Text(
 | 
			
		||||
          "Login",
 | 
			
		||||
          "login_form_button_text",
 | 
			
		||||
          style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
			
		||||
        ));
 | 
			
		||||
        ).tr());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -47,8 +48,8 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
        onChanged: (value) {
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
 | 
			
		||||
        },
 | 
			
		||||
        decoration: const InputDecoration(
 | 
			
		||||
          hintText: 'Search your photos',
 | 
			
		||||
        decoration: InputDecoration(
 | 
			
		||||
          hintText: 'search_bar_hint'.tr(),
 | 
			
		||||
          enabledBorder: UnderlineInputBorder(
 | 
			
		||||
            borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ class ThumbnailWithInfo extends StatelessWidget {
 | 
			
		||||
                child: SizedBox(
 | 
			
		||||
                  width: MediaQuery.of(context).size.width / 3,
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    textInfo.capitalizeFirstLetter(),
 | 
			
		||||
                    textInfo,
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                      color: Colors.white,
 | 
			
		||||
                      fontWeight: FontWeight.bold,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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:hive_flutter/hive_flutter.dart';
 | 
			
		||||
@ -82,7 +83,7 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
                      return ThumbnailWithInfo(
 | 
			
		||||
                        imageUrl:
 | 
			
		||||
                            'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
 | 
			
		||||
                        textInfo: 'No Places Info Available',
 | 
			
		||||
                        textInfo: 'search_page_no_places'.tr(),
 | 
			
		||||
                        onTap: () {},
 | 
			
		||||
                      );
 | 
			
		||||
                    }),
 | 
			
		||||
@ -134,7 +135,7 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
                      return ThumbnailWithInfo(
 | 
			
		||||
                        imageUrl:
 | 
			
		||||
                            'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
 | 
			
		||||
                        textInfo: 'No Object Info Available',
 | 
			
		||||
                        textInfo: 'sarch_no_objects'.tr(),
 | 
			
		||||
                        onTap: () {},
 | 
			
		||||
                      );
 | 
			
		||||
                    }),
 | 
			
		||||
@ -158,20 +159,20 @@ class SearchPage extends HookConsumerWidget {
 | 
			
		||||
          children: [
 | 
			
		||||
            ListView(
 | 
			
		||||
              children: [
 | 
			
		||||
                const Padding(
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: EdgeInsets.all(16.0),
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    "Places",
 | 
			
		||||
                  child: const Text(
 | 
			
		||||
                    "search_page_places",
 | 
			
		||||
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
			
		||||
                  ),
 | 
			
		||||
                  ).tr(),
 | 
			
		||||
                ),
 | 
			
		||||
                _buildPlaces(),
 | 
			
		||||
                const Padding(
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: EdgeInsets.all(16.0),
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    "Things",
 | 
			
		||||
                  child: const  Text(
 | 
			
		||||
                    "search_page_things",
 | 
			
		||||
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
			
		||||
                  ),
 | 
			
		||||
                  ).tr(),
 | 
			
		||||
                ),
 | 
			
		||||
                _buildThings()
 | 
			
		||||
              ],
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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:flutter_spinkit/flutter_spinkit.dart';
 | 
			
		||||
@ -66,8 +67,8 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
        onChanged: (value) {
 | 
			
		||||
          ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
 | 
			
		||||
        },
 | 
			
		||||
        decoration: const InputDecoration(
 | 
			
		||||
          hintText: 'New Search',
 | 
			
		||||
        decoration: InputDecoration(
 | 
			
		||||
          hintText: 'search_result_page_new_search_hint'.tr(),
 | 
			
		||||
          enabledBorder: UnderlineInputBorder(
 | 
			
		||||
            borderSide: BorderSide(color: Colors.transparent),
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
 | 
			
		||||
@ -59,7 +60,7 @@ class AlbumTitleTextField extends ConsumerWidget {
 | 
			
		||||
          borderSide: const BorderSide(color: Colors.transparent),
 | 
			
		||||
          borderRadius: BorderRadius.circular(10),
 | 
			
		||||
        ),
 | 
			
		||||
        hintText: 'Add a title',
 | 
			
		||||
        hintText: 'share_add_title'.tr(),
 | 
			
		||||
        focusColor: Colors.grey[300],
 | 
			
		||||
        fillColor: Colors.grey[200],
 | 
			
		||||
        filled: isAlbumTitleTextFieldFocus.value,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
@ -45,7 +46,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
      } else {
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
          msg: "Failed to delete album",
 | 
			
		||||
          msg: "album_viewer_appbar_share_err_delete".tr(),
 | 
			
		||||
          toastType: ToastType.error,
 | 
			
		||||
          gravity: ToastGravity.BOTTOM,
 | 
			
		||||
        );
 | 
			
		||||
@ -67,7 +68,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
        Navigator.pop(context);
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
          msg: "Failed to leave album",
 | 
			
		||||
          msg: "album_viewer_appbar_share_err_leave".tr(),
 | 
			
		||||
          toastType: ToastType.error,
 | 
			
		||||
          gravity: ToastGravity.BOTTOM,
 | 
			
		||||
        );
 | 
			
		||||
@ -93,7 +94,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
        Navigator.pop(context);
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
          msg: "There are problems in removing assets from album",
 | 
			
		||||
          msg: "album_viewer_appbar_share_err_remove".tr(),
 | 
			
		||||
          toastType: ToastType.error,
 | 
			
		||||
          gravity: ToastGravity.BOTTOM,
 | 
			
		||||
        );
 | 
			
		||||
@ -108,9 +109,9 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
          return ListTile(
 | 
			
		||||
            leading: const Icon(Icons.delete_sweep_rounded),
 | 
			
		||||
            title: const Text(
 | 
			
		||||
              'Remove from album',
 | 
			
		||||
              'album_viewer_appbar_share_remove',
 | 
			
		||||
              style: TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
            onTap: () => _onRemoveFromAlbumPressed(albumId),
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
@ -121,18 +122,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
          return ListTile(
 | 
			
		||||
            leading: const Icon(Icons.delete_forever_rounded),
 | 
			
		||||
            title: const Text(
 | 
			
		||||
              'Delete album',
 | 
			
		||||
              'album_viewer_appbar_share_delete',
 | 
			
		||||
              style: TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
            onTap: () => _onDeleteAlbumPressed(albumId),
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          return ListTile(
 | 
			
		||||
            leading: const Icon(Icons.person_remove_rounded),
 | 
			
		||||
            title: const Text(
 | 
			
		||||
              'Leave album',
 | 
			
		||||
              'album_viewer_appbar_share_leave',
 | 
			
		||||
              style: TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
            onTap: () => _onLeaveAlbumPressed(albumId),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
@ -176,7 +177,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
            if (!isSuccess) {
 | 
			
		||||
              ImmichToast.show(
 | 
			
		||||
                context: context,
 | 
			
		||||
                msg: "Failed to change album title",
 | 
			
		||||
                msg: "album_viewer_appbar_share_err_title".tr(),
 | 
			
		||||
                gravity: ToastGravity.BOTTOM,
 | 
			
		||||
                toastType: ToastType.error,
 | 
			
		||||
              );
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -74,7 +75,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
 | 
			
		||||
        focusColor: Colors.grey[300],
 | 
			
		||||
        fillColor: Colors.grey[200],
 | 
			
		||||
        filled: titleFocusNode.hasFocus,
 | 
			
		||||
        hintText: 'Add a title',
 | 
			
		||||
        hintText: 'share_add_title'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:immich_mobile/routing/router.dart';
 | 
			
		||||
 | 
			
		||||
@ -51,10 +52,10 @@ class SharingSliverAppBar extends StatelessWidget {
 | 
			
		||||
                      size: 20,
 | 
			
		||||
                    ),
 | 
			
		||||
                    label: const Text(
 | 
			
		||||
                      "Create shared album",
 | 
			
		||||
                      "sharing_silver_appbar_create_shared_album",
 | 
			
		||||
                      style:
 | 
			
		||||
                          TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ).tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
@ -73,10 +74,10 @@ class SharingSliverAppBar extends StatelessWidget {
 | 
			
		||||
                      size: 20,
 | 
			
		||||
                    ),
 | 
			
		||||
                    label: const Text(
 | 
			
		||||
                      "Share with partner",
 | 
			
		||||
                      "sharing_silver_appbar_share_partner",
 | 
			
		||||
                      style:
 | 
			
		||||
                          TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ).tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -204,13 +205,13 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
              AlbumActionOutlinedButton(
 | 
			
		||||
                iconData: Icons.add_photo_alternate_outlined,
 | 
			
		||||
                onPressed: () => _onAddPhotosPressed(albumInfo),
 | 
			
		||||
                labelText: "Add photos",
 | 
			
		||||
                labelText: "share_add_photos".tr(),
 | 
			
		||||
              ),
 | 
			
		||||
              if (userId == albumInfo.ownerId)
 | 
			
		||||
                AlbumActionOutlinedButton(
 | 
			
		||||
                  iconData: Icons.person_add_alt_rounded,
 | 
			
		||||
                  onPressed: () => _onAddUsersPressed(albumInfo),
 | 
			
		||||
                  labelText: "Add users",
 | 
			
		||||
                  labelText: "album_viewer_page_share_add_users".tr(),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -65,9 +66,9 @@ class AssetSelectionPage extends HookConsumerWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        title: selectedAssets.isEmpty
 | 
			
		||||
            ? const Text(
 | 
			
		||||
                'Add photos',
 | 
			
		||||
                'share_add_photos',
 | 
			
		||||
                style: TextStyle(fontSize: 18),
 | 
			
		||||
              )
 | 
			
		||||
              ).tr()
 | 
			
		||||
            : Text(
 | 
			
		||||
                _buildAssetCountText(),
 | 
			
		||||
                style: const TextStyle(fontSize: 18),
 | 
			
		||||
@ -86,9 +87,9 @@ class AssetSelectionPage extends HookConsumerWidget {
 | 
			
		||||
                AutoRouter.of(context).pop(payload);
 | 
			
		||||
              },
 | 
			
		||||
              child: const Text(
 | 
			
		||||
                "Add",
 | 
			
		||||
                "share_add",
 | 
			
		||||
                style: TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
              ),
 | 
			
		||||
              ).tr(),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -64,13 +65,13 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    _buildTitle() {
 | 
			
		||||
      if (selectedAssets.isEmpty) {
 | 
			
		||||
        return const SliverToBoxAdapter(
 | 
			
		||||
        return SliverToBoxAdapter(
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: EdgeInsets.only(top: 200, left: 18),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              'ADD ASSETS',
 | 
			
		||||
              'create_shared_album_page_share_add_assets',
 | 
			
		||||
              style: TextStyle(fontSize: 12),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
@ -97,12 +98,12 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
              label: Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(left: 8.0),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  'Select Photos',
 | 
			
		||||
                  'create_shared_album_page_share_select_photos',
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                      fontSize: 16,
 | 
			
		||||
                      color: Colors.grey[700],
 | 
			
		||||
                      fontWeight: FontWeight.bold),
 | 
			
		||||
                ),
 | 
			
		||||
                ).tr(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
@ -123,7 +124,7 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
              AlbumActionOutlinedButton(
 | 
			
		||||
                iconData: Icons.add_photo_alternate_outlined,
 | 
			
		||||
                onPressed: _onSelectPhotosButtonPressed,
 | 
			
		||||
                labelText: "Add photos",
 | 
			
		||||
                labelText: "share_add_photos".tr(),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
@ -169,16 +170,16 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
              },
 | 
			
		||||
              icon: const Icon(Icons.close_rounded)),
 | 
			
		||||
          title: const Text(
 | 
			
		||||
            'Create album',
 | 
			
		||||
            'share_create_album',
 | 
			
		||||
            style: TextStyle(color: Colors.black),
 | 
			
		||||
          ),
 | 
			
		||||
          ).tr(),
 | 
			
		||||
          actions: [
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: albumTitleController.text.isNotEmpty
 | 
			
		||||
                  ? _showSelectUserPage
 | 
			
		||||
                  : null,
 | 
			
		||||
              child: const Text(
 | 
			
		||||
                'Share',
 | 
			
		||||
              child: Text(
 | 
			
		||||
                'create_shared_album_page_share'.tr(),
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -68,10 +69,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
          Wrap(
 | 
			
		||||
            children: [...usersChip],
 | 
			
		||||
          ),
 | 
			
		||||
          const Padding(
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: EdgeInsets.all(16.0),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              'Suggestions',
 | 
			
		||||
              'select_additional_user_for_sharing_page_suggestions'.tr(),
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                  fontSize: 14,
 | 
			
		||||
                  color: Colors.grey,
 | 
			
		||||
@ -112,9 +113,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text(
 | 
			
		||||
          'Invite to album',
 | 
			
		||||
          'share_invite',
 | 
			
		||||
          style: TextStyle(color: Colors.black),
 | 
			
		||||
        ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        centerTitle: false,
 | 
			
		||||
        leading: IconButton(
 | 
			
		||||
@ -128,9 +129,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
            onPressed:
 | 
			
		||||
                sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
 | 
			
		||||
            child: const Text(
 | 
			
		||||
              "Add",
 | 
			
		||||
              "share_add",
 | 
			
		||||
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
          )
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
@ -36,8 +37,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
            .navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const ScaffoldMessenger(
 | 
			
		||||
          child: SnackBar(content: Text('Failed to create album')));
 | 
			
		||||
      ScaffoldMessenger(child: SnackBar(content: Text('select_user_for_sharing_page_err_album').tr()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _buildTileIcon(User user) {
 | 
			
		||||
@ -84,15 +84,15 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
          Wrap(
 | 
			
		||||
            children: [...usersChip],
 | 
			
		||||
          ),
 | 
			
		||||
          const Padding(
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: EdgeInsets.all(16.0),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              'Suggestions',
 | 
			
		||||
              'share_suggestions',
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                  fontSize: 14,
 | 
			
		||||
                  color: Colors.grey,
 | 
			
		||||
                  fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
            ).tr(),
 | 
			
		||||
          ),
 | 
			
		||||
          ListView.builder(
 | 
			
		||||
            shrinkWrap: true,
 | 
			
		||||
@ -128,9 +128,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: const Text(
 | 
			
		||||
          'Invite to album',
 | 
			
		||||
          'share_invite',
 | 
			
		||||
          style: TextStyle(color: Colors.black),
 | 
			
		||||
        ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        elevation: 0,
 | 
			
		||||
        centerTitle: false,
 | 
			
		||||
        leading: IconButton(
 | 
			
		||||
@ -144,9 +144,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 | 
			
		||||
              onPressed:
 | 
			
		||||
                  sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
 | 
			
		||||
              child: const Text(
 | 
			
		||||
                "Create Album",
 | 
			
		||||
                "share_create_album",
 | 
			
		||||
                style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
			
		||||
              ))
 | 
			
		||||
              ).tr())
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: suggestedShareUsers.when(
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
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:hive/hive.dart';
 | 
			
		||||
@ -104,20 +105,20 @@ class SharingPage extends HookConsumerWidget {
 | 
			
		||||
                  Padding(
 | 
			
		||||
                    padding: const EdgeInsets.all(8.0),
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      'EMPTY LIST',
 | 
			
		||||
                      'sharing_page_empty_list',
 | 
			
		||||
                      style: TextStyle(
 | 
			
		||||
                        fontSize: 12,
 | 
			
		||||
                        color: Theme.of(context).primaryColor,
 | 
			
		||||
                        fontWeight: FontWeight.bold,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ).tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  Padding(
 | 
			
		||||
                    padding: const EdgeInsets.all(8.0),
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      'Create shared albums to share photos and videos with people in your network.',
 | 
			
		||||
                      'sharing_page_description',
 | 
			
		||||
                      style: TextStyle(fontSize: 12, color: Colors.grey[700]),
 | 
			
		||||
                    ),
 | 
			
		||||
                    ).tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
@ -131,15 +132,15 @@ class SharingPage extends HookConsumerWidget {
 | 
			
		||||
      body: CustomScrollView(
 | 
			
		||||
        slivers: [
 | 
			
		||||
          const SharingSliverAppBar(),
 | 
			
		||||
          const SliverPadding(
 | 
			
		||||
          SliverPadding(
 | 
			
		||||
            padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
 | 
			
		||||
            sliver: SliverToBoxAdapter(
 | 
			
		||||
              child: Text(
 | 
			
		||||
                "Shared albums",
 | 
			
		||||
                "sharing_page_album",
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              ).tr(),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          sharedAlbums.isNotEmpty
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
@ -41,13 +42,16 @@ class TabControllerPage extends ConsumerWidget {
 | 
			
		||||
                    onTap: (index) {
 | 
			
		||||
                      tabsRouter.setActiveIndex(index);
 | 
			
		||||
                    },
 | 
			
		||||
                    items: const [
 | 
			
		||||
                    items: [
 | 
			
		||||
                      BottomNavigationBarItem(
 | 
			
		||||
                          label: 'Photos', icon: Icon(Icons.photo)),
 | 
			
		||||
                          label: 'tab_controller_nav_photos'.tr(),
 | 
			
		||||
                          icon: const Icon(Icons.photo)),
 | 
			
		||||
                      BottomNavigationBarItem(
 | 
			
		||||
                          label: 'Search', icon: Icon(Icons.search)),
 | 
			
		||||
                          label: 'tab_controller_nav_search'.tr(),
 | 
			
		||||
                          icon: const Icon(Icons.search)),
 | 
			
		||||
                      BottomNavigationBarItem(
 | 
			
		||||
                          label: 'Sharing', icon: Icon(Icons.group_outlined)),
 | 
			
		||||
                          label: 'tab_controller_nav_sharing'.tr(),
 | 
			
		||||
                          icon: const Icon(Icons.group_outlined)),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/gestures.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
@ -40,14 +41,14 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            const Text(
 | 
			
		||||
                              "New Server Version Available 🎉",
 | 
			
		||||
                              "version_announcement_overlay_title",
 | 
			
		||||
                              style: TextStyle(
 | 
			
		||||
                                fontSize: 16,
 | 
			
		||||
                                fontFamily: 'WorkSans',
 | 
			
		||||
                                fontWeight: FontWeight.bold,
 | 
			
		||||
                                color: Colors.indigo,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            ).tr(),
 | 
			
		||||
                            Padding(
 | 
			
		||||
                              padding: const EdgeInsets.only(top: 16.0),
 | 
			
		||||
                              child: RichText(
 | 
			
		||||
@ -58,9 +59,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                                      color: Colors.black87,
 | 
			
		||||
                                      height: 1.2),
 | 
			
		||||
                                  children: <TextSpan>[
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text:
 | 
			
		||||
                                          'Hi friend, there is a new release of',
 | 
			
		||||
                                    TextSpan(
 | 
			
		||||
                                      text: 'version_announcement_overlay_text_1'.tr(),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text: ' Immich ',
 | 
			
		||||
@ -70,22 +70,21 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                                        fontWeight: FontWeight.bold,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text:
 | 
			
		||||
                                          "please take your time to visit the ",
 | 
			
		||||
                                    TextSpan(
 | 
			
		||||
                                      text: "version_announcement_overlay_text_2".tr(),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    TextSpan(
 | 
			
		||||
                                      text: "release note",
 | 
			
		||||
                                      text: "version_announcement_overlay_release_notes"
 | 
			
		||||
                                          .tr(),
 | 
			
		||||
                                      style: const TextStyle(
 | 
			
		||||
                                        decoration: TextDecoration.underline,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                      recognizer: TapGestureRecognizer()
 | 
			
		||||
                                        ..onTap = goToReleaseNote,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text:
 | 
			
		||||
                                          " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    TextSpan(
 | 
			
		||||
                                      text: "version_announcement_overlay_text_3".tr(),
 | 
			
		||||
                                    )
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
@ -104,13 +103,12 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  onPressed: onAcknowledgeTapped,
 | 
			
		||||
                                  child: const Text(
 | 
			
		||||
                                  "Acknowledge",
 | 
			
		||||
                                    "version_announcement_overlay_ack",
 | 
			
		||||
                                    style: TextStyle(
 | 
			
		||||
                                      fontSize: 14,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                                  ).tr()),
 | 
			
		||||
                            )
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
 | 
			
		||||
@ -253,6 +253,20 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.0.6"
 | 
			
		||||
  easy_localization:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: easy_localization
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.1"
 | 
			
		||||
  easy_logger:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: easy_logger
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.0.2"
 | 
			
		||||
  equatable:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@ -335,6 +349,11 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.1"
 | 
			
		||||
  flutter_localizations:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description: flutter
 | 
			
		||||
    source: sdk
 | 
			
		||||
    version: "0.0.0"
 | 
			
		||||
  flutter_map:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@ -842,6 +861,62 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.27.3"
 | 
			
		||||
  shared_preferences:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.15"
 | 
			
		||||
  shared_preferences_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_android
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.12"
 | 
			
		||||
  shared_preferences_ios:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_ios
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.1"
 | 
			
		||||
  shared_preferences_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_linux
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.1"
 | 
			
		||||
  shared_preferences_macos:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_macos
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.4"
 | 
			
		||||
  shared_preferences_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_platform_interface
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.0"
 | 
			
		||||
  shared_preferences_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_web
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.4"
 | 
			
		||||
  shared_preferences_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_windows
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.1"
 | 
			
		||||
  shelf:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ dependencies:
 | 
			
		||||
  url_launcher: ^6.1.3
 | 
			
		||||
  http: 0.13.4
 | 
			
		||||
  cancellation_token_http: ^1.1.0
 | 
			
		||||
  easy_localization: ^3.0.1
 | 
			
		||||
 | 
			
		||||
  path: ^1.8.1
 | 
			
		||||
  path_provider: ^2.0.11
 | 
			
		||||
@ -59,6 +60,7 @@ flutter:
 | 
			
		||||
  uses-material-design: true
 | 
			
		||||
  assets:
 | 
			
		||||
    - assets/
 | 
			
		||||
    - assets/i18n/
 | 
			
		||||
  fonts:
 | 
			
		||||
    - family: WorkSans
 | 
			
		||||
      fonts:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								mobile/scripts/check_i18n_keys.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mobile/scripts/check_i18n_keys.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import json
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    with open('assets/i18n/en-US.json', 'r') as f:
 | 
			
		||||
        data = json.load(f)
 | 
			
		||||
 | 
			
		||||
        for k in data.keys():
 | 
			
		||||
            print(k)
 | 
			
		||||
            sp = subprocess.run(['sh', '-c', f'grep -r --include="*.dart" "{k}"'])
 | 
			
		||||
 | 
			
		||||
            if sp.returncode != 0:
 | 
			
		||||
                print("Not found in source code!")
 | 
			
		||||
                return 1
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										19
									
								
								mobile/scripts/check_key_uniform.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								mobile/scripts/check_key_uniform.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import json
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    print("CHECK GERMAN TRANSLATIONS")
 | 
			
		||||
    with open('assets/i18n/de-DE.json', 'r') as f:
 | 
			
		||||
        data = json.load(f)
 | 
			
		||||
 | 
			
		||||
        for k in data.keys():
 | 
			
		||||
            print(k)
 | 
			
		||||
            sp = subprocess.run(['sh', '-c', f'grep -r --include="./assets/i18n/en-US.json" "{k}"'])
 | 
			
		||||
 | 
			
		||||
            if sp.returncode != 0:
 | 
			
		||||
                print(f"Outdated Key! {k}")
 | 
			
		||||
                return 1
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user