diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 200b46ccc6..9da8673865 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -43,7 +43,11 @@ "asset_list_layout_settings_group_by_month": "Month", "asset_list_layout_settings_group_by_month_day": "Month + day", "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", + "asset_list_settings_title": "Timeline", + "asset_list_group_by_sub_title": "Group by", + "asset_list_layout_sub_title": "Layout", + "asset_viewer_settings_title": "Asset Viewer", + "preferences_settings_title": "Preferences", "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.", diff --git a/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart b/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart index 449d5b6c8c..df6ee779cc 100644 --- a/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart +++ b/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart @@ -7,7 +7,7 @@ part of 'asset_people.provider.dart'; // ************************************************************************** String _$assetPeopleNotifierHash() => - r'192a4ee188f781000fe43f1675c49e1081ccc631'; + r'9835b180984a750c91e923e7b64dbda94f6d7574'; /// Copied from Dart SDK class _SystemHash { @@ -34,7 +34,7 @@ abstract class _$AssetPeopleNotifier extends BuildlessAutoDisposeAsyncNotifier< List> { late final Asset asset; - Future> build( + FutureOr> build( Asset asset, ); } @@ -127,7 +127,7 @@ class AssetPeopleNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl< final Asset asset; @override - Future> runNotifierBuild( + FutureOr> runNotifierBuild( covariant AssetPeopleNotifier notifier, ) { return notifier.build( diff --git a/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart b/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart index a9b287e953..e447304b36 100644 --- a/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart +++ b/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart @@ -7,7 +7,7 @@ part of 'video_player_controller_provider.dart'; // ************************************************************************** String _$videoPlayerControllerHash() => - r'72b45de66542021717807655e25ec92d78d80eec'; + r'40b31f7b1a73fab84c311b0f06bedf5322143cd9'; /// Copied from Dart SDK class _SystemHash { diff --git a/mobile/lib/modules/backup/providers/backup_verification.provider.dart b/mobile/lib/modules/backup/providers/backup_verification.provider.dart new file mode 100644 index 0000000000..1cc5379131 --- /dev/null +++ b/mobile/lib/modules/backup/providers/backup_verification.provider.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; +import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +part 'backup_verification.provider.g.dart'; + +@riverpod +class BackupVerification extends _$BackupVerification { + @override + bool build() => false; + + void performBackupCheck(BuildContext context) async { + try { + state = true; + final backupState = ref.read(backupProvider); + + if (backupState.allUniqueAssets.length > + backupState.selectedAlbumsBackupAssetsIds.length) { + if (context.mounted) { + ImmichToast.show( + context: context, + msg: "Backup all assets before starting this check!", + toastType: ToastType.error, + ); + } + return; + } + final connection = await Connectivity().checkConnectivity(); + if (connection != ConnectivityResult.wifi) { + if (context.mounted) { + ImmichToast.show( + context: context, + msg: "Make sure to be connected to unmetered Wi-Fi", + toastType: ToastType.error, + ); + } + return; + } + WakelockPlus.enable(); + + const limit = 100; + final toDelete = await ref + .read(backupVerificationServiceProvider) + .findWronglyBackedUpAssets(limit: limit); + if (toDelete.isEmpty) { + if (context.mounted) { + ImmichToast.show( + context: context, + msg: "Did not find any corrupt asset backups!", + toastType: ToastType.success, + ); + } + } else { + if (context.mounted) { + await showDialog( + context: context, + builder: (ctx) => ConfirmDialog( + onOk: () => _performDeletion(context, toDelete), + title: "Corrupt backups!", + ok: "Delete", + content: + "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " + "Run the check again to find more.\n" + "Do you want to delete the corrupt asset backups now?", + ), + ); + } + } + } finally { + WakelockPlus.disable(); + state = false; + } + } + + Future _performDeletion( + BuildContext context, + List assets, + ) async { + try { + state = true; + if (context.mounted) { + ImmichToast.show( + context: context, + msg: "Deleting ${assets.length} assets on the server...", + ); + } + await ref.read(assetProvider.notifier).deleteAssets(assets, force: true); + if (context.mounted) { + ImmichToast.show( + context: context, + msg: "Deleted ${assets.length} assets on the server. " + "You can now start a manual backup", + toastType: ToastType.success, + ); + } + } finally { + state = false; + } + } +} diff --git a/mobile/lib/modules/backup/providers/backup_verification.provider.g.dart b/mobile/lib/modules/backup/providers/backup_verification.provider.g.dart new file mode 100644 index 0000000000..f222c9bd83 --- /dev/null +++ b/mobile/lib/modules/backup/providers/backup_verification.provider.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'backup_verification.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$backupVerificationHash() => + r'b691e0cc27856eef189258d3c102cc73ce4812a4'; + +/// See also [BackupVerification]. +@ProviderFor(BackupVerification) +final backupVerificationProvider = + AutoDisposeNotifierProvider.internal( + BackupVerification.new, + name: r'backupVerificationProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$backupVerificationHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$BackupVerification = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/backup/views/backup_options_page.dart b/mobile/lib/modules/backup/views/backup_options_page.dart index 8144f1b8ed..b37ded6a6a 100644 --- a/mobile/lib/modules/backup/views/backup_options_page.dart +++ b/mobile/lib/modules/backup/views/backup_options_page.dart @@ -1,487 +1,12 @@ -import 'dart:io'; - import 'package:auto_route/auto_route.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; -import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; -import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; -import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; -import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart'; -import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -import 'package:immich_mobile/shared/ui/immich_toast.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart'; @RoutePage() -class BackupOptionsPage extends HookConsumerWidget { +class BackupOptionsPage extends StatelessWidget { const BackupOptionsPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - BackUpState backupState = ref.watch(backupProvider); - final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings; - final settingsService = ref.watch(appSettingsServiceProvider); - final showBackupFix = Platform.isAndroid && - settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting); - final ignoreIcloudAssets = useState( - settingsService.getSetting(AppSettingsEnum.ignoreIcloudAssets), - ); - final appRefreshDisabled = - Platform.isIOS && settings?.appRefreshEnabled != true; - final checkInProgress = useState(false); - - Future performDeletion(List assets) async { - try { - checkInProgress.value = true; - ImmichToast.show( - context: context, - msg: "Deleting ${assets.length} assets on the server...", - ); - await ref - .read(assetProvider.notifier) - .deleteAssets(assets, force: true); - ImmichToast.show( - context: context, - msg: "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } finally { - checkInProgress.value = false; - } - } - - void performBackupCheck() async { - try { - checkInProgress.value = true; - if (backupState.allUniqueAssets.length > - backupState.selectedAlbumsBackupAssetsIds.length) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - return; - } - final connection = await Connectivity().checkConnectivity(); - if (connection != ConnectivityResult.wifi) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - return; - } - WakelockPlus.enable(); - const limit = 100; - final toDelete = await ref - .read(backupVerificationServiceProvider) - .findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } else { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => performDeletion(toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } finally { - WakelockPlus.disable(); - checkInProgress.value = false; - } - } - - Widget buildCheckCorruptBackups() { - return ListTile( - leading: Icon( - Icons.warning_rounded, - color: context.primaryColor, - ), - title: const Text( - "Check for corrupt asset backups", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - isThreeLine: true, - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text("Run this check only over Wi-Fi and once all assets " - "have been backed-up. The procedure might take a few minutes."), - ElevatedButton( - onPressed: checkInProgress.value ? null : performBackupCheck, - child: checkInProgress.value - ? const CircularProgressIndicator() - : const Text("Perform check"), - ), - ], - ), - ); - } - - void showErrorToUser(String msg) { - final snackBar = SnackBar( - content: Text( - msg.tr(), - style: context.textTheme.bodyLarge?.copyWith( - color: context.primaryColor, - ), - ), - backgroundColor: Colors.red, - ); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - } - - void showBatteryOptimizationInfoToUser() { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - title: const Text( - 'backup_controller_page_background_battery_info_title', - ).tr(), - content: SingleChildScrollView( - child: const Text( - 'backup_controller_page_background_battery_info_message', - ).tr(), - ), - actions: [ - ElevatedButton( - onPressed: () => launchUrl( - Uri.parse('https://dontkillmyapp.com'), - mode: LaunchMode.externalApplication, - ), - child: const Text( - "backup_controller_page_background_battery_info_link", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - ), - ElevatedButton( - child: const Text( - 'backup_controller_page_background_battery_info_ok', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ).tr(), - onPressed: () { - context.pop(); - }, - ), - ], - ); - }, - ); - } - - Widget buildBackgroundBackupController() { - final bool isBackgroundEnabled = backupState.backgroundBackup; - final bool isWifiRequired = backupState.backupRequireWifi; - final bool isChargingRequired = backupState.backupRequireCharging; - final Color activeColor = context.primaryColor; - - String formatBackupDelaySliderValue(double v) { - if (v == 0.0) { - return 'setting_notifications_notify_seconds'.tr(args: const ['5']); - } else if (v == 1.0) { - return 'setting_notifications_notify_seconds'.tr(args: const ['30']); - } else if (v == 2.0) { - return 'setting_notifications_notify_minutes'.tr(args: const ['2']); - } else { - return 'setting_notifications_notify_minutes'.tr(args: const ['10']); - } - } - - int backupDelayToMilliseconds(double v) { - if (v == 0.0) { - return 5000; - } else if (v == 1.0) { - return 30000; - } else if (v == 2.0) { - return 120000; - } else { - return 600000; - } - } - - double backupDelayToSliderValue(int ms) { - if (ms == 5000) { - return 0.0; - } else if (ms == 30000) { - return 1.0; - } else if (ms == 120000) { - return 2.0; - } else { - return 3.0; - } - } - - final triggerDelay = - useState(backupDelayToSliderValue(backupState.backupTriggerDelay)); - - return Column( - children: [ - ListTile( - isThreeLine: true, - leading: isBackgroundEnabled - ? Icon( - Icons.cloud_sync_rounded, - color: activeColor, - ) - : const Icon(Icons.cloud_sync_rounded), - title: Text( - isBackgroundEnabled - ? "backup_controller_page_background_is_on" - : "backup_controller_page_background_is_off", - style: context.textTheme.titleSmall, - ).tr(), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isBackgroundEnabled) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: const Text( - "backup_controller_page_background_description", - ).tr(), - ), - if (isBackgroundEnabled) - SwitchListTile.adaptive( - title: const Text("backup_controller_page_background_wifi") - .tr(), - secondary: Icon( - Icons.wifi, - color: isWifiRequired ? activeColor : null, - ), - dense: true, - activeColor: activeColor, - value: isWifiRequired, - onChanged: (isChecked) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - requireWifi: isChecked, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ), - if (isBackgroundEnabled) - SwitchListTile.adaptive( - title: - const Text("backup_controller_page_background_charging") - .tr(), - secondary: Icon( - Icons.charging_station, - color: isChargingRequired ? activeColor : null, - ), - dense: true, - activeColor: activeColor, - value: isChargingRequired, - onChanged: (isChecked) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - requireCharging: isChecked, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ), - if (isBackgroundEnabled && Platform.isAndroid) - ListTile( - isThreeLine: false, - dense: true, - title: const Text( - 'backup_controller_page_background_delay', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ).tr( - args: [formatBackupDelaySliderValue(triggerDelay.value)], - ), - subtitle: Slider( - value: triggerDelay.value, - onChanged: (double v) => triggerDelay.value = v, - onChangeEnd: (double v) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - triggerDelay: backupDelayToMilliseconds(v), - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - max: 3.0, - divisions: 3, - label: formatBackupDelaySliderValue(triggerDelay.value), - activeColor: context.primaryColor, - ), - ), - ElevatedButton( - onPressed: () => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - enabled: !isBackgroundEnabled, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - child: Text( - isBackgroundEnabled - ? "backup_controller_page_background_turn_off" - : "backup_controller_page_background_turn_on", - style: context.textTheme.labelLarge?.copyWith( - color: context.isDarkTheme ? Colors.black : Colors.white, - ), - ).tr(), - ), - ], - ), - ), - if (isBackgroundEnabled && Platform.isIOS) - FutureBuilder( - future: ref - .read(backgroundServiceProvider) - .getIOSBackgroundAppRefreshEnabled(), - builder: (context, snapshot) { - final enabled = snapshot.data; - // If it's not enabled, show them some kind of alert that says - // background refresh is not enabled - if (enabled != null && !enabled) {} - // If it's enabled, no need to bother them - return Container(); - }, - ), - if (Platform.isIOS && isBackgroundEnabled && settings != null) - IosDebugInfoTile( - settings: settings, - ), - ], - ); - } - - Widget buildBackgroundAppRefreshWarning() { - return ListTile( - isThreeLine: true, - leading: const Icon( - Icons.task_outlined, - ), - title: const Text( - 'backup_controller_page_background_app_refresh_disabled_title', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ).tr(), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: const Text( - 'backup_controller_page_background_app_refresh_disabled_content', - ).tr(), - ), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text( - 'backup_controller_page_background_app_refresh_enable_button_text', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ).tr(), - ), - ], - ), - ); - } - - ListTile buildAutoBackupController() { - final isAutoBackup = backupState.autoBackup; - final backUpOption = isAutoBackup - ? "backup_controller_page_status_on".tr() - : "backup_controller_page_status_off".tr(); - final backupBtnText = isAutoBackup - ? "backup_controller_page_turn_off".tr() - : "backup_controller_page_turn_on".tr(); - return ListTile( - isThreeLine: true, - leading: isAutoBackup - ? Icon( - Icons.cloud_done_rounded, - color: context.primaryColor, - ) - : const Icon(Icons.cloud_off_rounded), - title: Text( - backUpOption, - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isAutoBackup) - const Text( - "backup_controller_page_desc_backup", - ).tr(), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: ElevatedButton( - onPressed: () => ref - .read(backupProvider.notifier) - .setAutoBackup(!isAutoBackup), - child: Text( - backupBtnText, - style: context.textTheme.labelLarge?.copyWith( - color: context.isDarkTheme ? Colors.black : Colors.white, - ), - ), - ), - ), - ], - ), - ), - ); - } - - void switchChanged(bool value) { - settingsService.setSetting(AppSettingsEnum.ignoreIcloudAssets, value); - ignoreIcloudAssets.value = value; - ref.invalidate(appSettingsServiceProvider); - } - - buildIgnoreIcloudAssetSetting() { - return [ - const Divider(), - SwitchListTile.adaptive( - title: const Text( - "Ignore iCloud photos", - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: const Text( - "Photos that are stored on iCloud will not be uploaded to the Immich server", - ), - value: ignoreIcloudAssets.value, - onChanged: switchChanged, - ), - ]; - } - + Widget build(BuildContext context) { return Scaffold( appBar: AppBar( elevation: 0, @@ -496,26 +21,7 @@ class BackupOptionsPage extends HookConsumerWidget { ), ), ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 32.0), - child: ListView( - children: [ - buildAutoBackupController(), - const Divider(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Platform.isIOS - ? (appRefreshDisabled - ? buildBackgroundAppRefreshWarning() - : buildBackgroundBackupController()) - : buildBackgroundBackupController(), - ), - if (Platform.isIOS) ...buildIgnoreIcloudAssetSetting(), - if (showBackupFix) const Divider(), - if (showBackupFix) buildCheckCorruptBackups(), - ], - ), - ), + body: const BackupSettings(), ); } } diff --git a/mobile/lib/modules/settings/models/store_model_here.txt b/mobile/lib/modules/settings/models/store_model_here.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/mobile/lib/modules/settings/ui/advanced_settings.dart b/mobile/lib/modules/settings/ui/advanced_settings.dart new file mode 100644 index 0000000000..65089ac5b3 --- /dev/null +++ b/mobile/lib/modules/settings/ui/advanced_settings.dart @@ -0,0 +1,69 @@ +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:immich_mobile/modules/settings/ui/local_storage_settings.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_slider_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; +import 'package:immich_mobile/shared/services/immich_logger.service.dart'; +import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; +import 'package:logging/logging.dart'; + +class AdvancedSettings extends HookConsumerWidget { + const AdvancedSettings({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + bool isLoggedIn = ref.read(currentUserProvider) != null; + + final advancedTroubleshooting = + useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final levelId = useAppSettingsState(AppSettingsEnum.logLevel); + final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage); + final allowSelfSignedSSLCert = + useAppSettingsState(AppSettingsEnum.allowSelfSignedSSLCert); + + final logLevel = Level.LEVELS[levelId.value].name; + + useValueChanged( + levelId.value, + (_, __) => ImmichLogger().level = Level.LEVELS[levelId.value], + ); + + final advancedSettings = [ + SettingsSwitchListTile( + enabled: true, + valueNotifier: advancedTroubleshooting, + title: "advanced_settings_troubleshooting_title".tr(), + subtitle: "advanced_settings_troubleshooting_subtitle".tr(), + ), + SettingsSliderListTile( + text: "advanced_settings_log_level_title".tr(args: [logLevel]), + valueNotifier: levelId, + maxValue: 8, + minValue: 1, + noDivisons: 7, + label: logLevel, + ), + SettingsSwitchListTile( + valueNotifier: preferRemote, + title: "advanced_settings_prefer_remote_title".tr(), + subtitle: "advanced_settings_prefer_remote_subtitle".tr(), + ), + const LocalStorageSettings(), + SettingsSwitchListTile( + enabled: !isLoggedIn, + valueNotifier: allowSelfSignedSSLCert, + title: "advanced_settings_self_signed_ssl_title".tr(), + subtitle: "advanced_settings_self_signed_ssl_subtitle".tr(), + onChanged: (_) => HttpOverrides.global = HttpSSLCertOverride(), + ), + ]; + + return SettingsSubPageScaffold(settings: advancedSettings); + } +} diff --git a/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart b/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart deleted file mode 100644 index d0397fe5a3..0000000000 --- a/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:io'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/shared/models/store.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; -import 'package:immich_mobile/shared/services/immich_logger.service.dart'; -import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; -import 'package:logging/logging.dart'; - -class AdvancedSettings extends HookConsumerWidget { - const AdvancedSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - bool isLoggedIn = Store.tryGet(StoreKey.currentUser) != null; - final appSettingService = ref.watch(appSettingsServiceProvider); - final isEnabled = - useState(AppSettingsEnum.advancedTroubleshooting.defaultValue); - final levelId = useState(AppSettingsEnum.logLevel.defaultValue); - final preferRemote = - useState(AppSettingsEnum.preferRemoteImage.defaultValue); - final allowSelfSignedSSLCert = - useState(AppSettingsEnum.allowSelfSignedSSLCert.defaultValue); - - useEffect( - () { - isEnabled.value = appSettingService.getSetting( - AppSettingsEnum.advancedTroubleshooting, - ); - levelId.value = appSettingService.getSetting(AppSettingsEnum.logLevel); - preferRemote.value = - appSettingService.getSetting(AppSettingsEnum.preferRemoteImage); - allowSelfSignedSSLCert.value = appSettingService - .getSetting(AppSettingsEnum.allowSelfSignedSSLCert); - return null; - }, - [], - ); - - final logLevel = Level.LEVELS[levelId.value].name; - - return ExpansionTile( - textColor: context.primaryColor, - title: Text( - "advanced_settings_tile_title", - style: context.textTheme.titleMedium, - ).tr(), - subtitle: const Text( - "advanced_settings_tile_subtitle", - ).tr(), - children: [ - SettingsSwitchListTile( - enabled: true, - appSettingService: appSettingService, - valueNotifier: isEnabled, - settingsEnum: AppSettingsEnum.advancedTroubleshooting, - title: "advanced_settings_troubleshooting_title".tr(), - subtitle: "advanced_settings_troubleshooting_subtitle".tr(), - ), - ListTile( - dense: true, - title: const Text( - "advanced_settings_log_level_title", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(args: [logLevel]), - subtitle: Slider( - value: levelId.value.toDouble(), - onChanged: (double v) => levelId.value = v.toInt(), - onChangeEnd: (double v) { - appSettingService.setSetting( - AppSettingsEnum.logLevel, - v.toInt(), - ); - ImmichLogger().level = Level.LEVELS[v.toInt()]; - }, - max: 8, - min: 1.0, - divisions: 7, - label: logLevel, - activeColor: context.primaryColor, - ), - ), - SettingsSwitchListTile( - appSettingService: appSettingService, - valueNotifier: preferRemote, - settingsEnum: AppSettingsEnum.preferRemoteImage, - title: "advanced_settings_prefer_remote_title".tr(), - subtitle: "advanced_settings_prefer_remote_subtitle".tr(), - ), - SettingsSwitchListTile( - enabled: !isLoggedIn, - appSettingService: appSettingService, - valueNotifier: allowSelfSignedSSLCert, - settingsEnum: AppSettingsEnum.allowSelfSignedSSLCert, - title: "advanced_settings_self_signed_ssl_title".tr(), - subtitle: "advanced_settings_self_signed_ssl_subtitle".tr(), - onChanged: (value) { - HttpOverrides.global = HttpSSLCertOverride(); - }, - ), - ], - ); - } -} diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_group_settings.dart new file mode 100644 index 0000000000..8595973f24 --- /dev/null +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_group_settings.dart @@ -0,0 +1,53 @@ +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/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_radio_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; + +class GroupSettings extends HookConsumerWidget { + const GroupSettings({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final groupByIndex = useAppSettingsState(AppSettingsEnum.groupAssetsBy); + final groupBy = GroupAssetsBy.values[groupByIndex.value]; + + void changeGroupValue(GroupAssetsBy? value) { + if (value != null) { + groupByIndex.value = value.index; + ref.invalidate(appSettingsServiceProvider); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "asset_list_group_by_sub_title".tr()), + SettingsRadioListTile( + groups: [ + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_by_month_day'.tr(), + value: GroupAssetsBy.day, + ), + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_by_month'.tr(), + value: GroupAssetsBy.month, + ), + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_automatically'.tr(), + value: GroupAssetsBy.auto, + ), + ], + groupBy: groupBy, + onRadioChanged: changeGroupValue, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart index 5ac22e131d..099d091e2f 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart @@ -1,11 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_slider_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; class LayoutSettings extends HookConsumerWidget { const LayoutSettings({ @@ -14,96 +15,27 @@ class LayoutSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - - final useDynamicLayout = useState(true); - final groupBy = useState(GroupAssetsBy.day); - - void switchChanged(bool value) { - appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value); - useDynamicLayout.value = value; - ref.invalidate(appSettingsServiceProvider); - } - - void changeGroupValue(GroupAssetsBy? value) { - if (value != null) { - appSettingService.setSetting( - AppSettingsEnum.groupAssetsBy, - value.index, - ); - groupBy.value = value; - ref.invalidate(appSettingsServiceProvider); - } - } - - useEffect( - () { - useDynamicLayout.value = - appSettingService.getSetting(AppSettingsEnum.dynamicLayout); - groupBy.value = GroupAssetsBy.values[ - appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; - - return null; - }, - [], - ); + final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout); + final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow); return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SwitchListTile.adaptive( - activeColor: context.primaryColor, - title: Text( - "asset_list_layout_settings_dynamic_layout_title", - style: context.textTheme.labelLarge, - ).tr(), - onChanged: switchChanged, - value: useDynamicLayout.value, + SettingsSubTitle(title: "asset_list_layout_sub_title".tr()), + SettingsSwitchListTile( + valueNotifier: useDynamicLayout, + title: "asset_list_layout_settings_dynamic_layout_title".tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), - const Divider( - indent: 18, - endIndent: 18, - ), - ListTile( - title: const Text( - "asset_list_layout_settings_group_by", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - RadioListTile( - activeColor: context.primaryColor, - title: Text( - "asset_list_layout_settings_group_by_month_day", - style: context.textTheme.labelLarge, - ).tr(), - value: GroupAssetsBy.day, - groupValue: groupBy.value, - onChanged: changeGroupValue, - controlAffinity: ListTileControlAffinity.trailing, - ), - RadioListTile( - activeColor: context.primaryColor, - title: Text( - "asset_list_layout_settings_group_by_month", - style: context.textTheme.labelLarge, - ).tr(), - value: GroupAssetsBy.month, - groupValue: groupBy.value, - onChanged: changeGroupValue, - controlAffinity: ListTileControlAffinity.trailing, - ), - RadioListTile( - activeColor: context.primaryColor, - title: Text( - "asset_list_layout_settings_group_automatically", - style: context.textTheme.labelLarge, - ).tr(), - value: GroupAssetsBy.auto, - groupValue: groupBy.value, - onChanged: changeGroupValue, - controlAffinity: ListTileControlAffinity.trailing, + SettingsSliderListTile( + valueNotifier: tilesPerRow, + text: 'theme_setting_asset_list_tiles_per_row_title' + .tr(args: ["${tilesPerRow.value}"]), + label: "${tilesPerRow.value}", + maxValue: 6, + minValue: 2, + noDivisons: 4, + onChangeEnd: (_) => ref.invalidate(appSettingsServiceProvider), ), ], ); diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart index a2ad73ec8e..82a64dc3ed 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart @@ -1,31 +1,37 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart'; -import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart'; -import 'asset_list_tiles_per_row.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_group_settings.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; +import 'asset_list_layout_settings.dart'; -class AssetListSettings extends StatelessWidget { +class AssetListSettings extends HookConsumerWidget { const AssetListSettings({ super.key, }); @override - Widget build(BuildContext context) { - return ExpansionTile( - textColor: context.primaryColor, - title: Text( - 'asset_list_settings_title', - style: context.textTheme.titleMedium, - ).tr(), - subtitle: const Text( - 'asset_list_settings_subtitle', - ).tr(), - children: const [ - TilesPerRow(), - StorageIndicator(), - LayoutSettings(), - ], + Widget build(BuildContext context, WidgetRef ref) { + final showStorageIndicator = + useAppSettingsState(AppSettingsEnum.storageIndicator); + + final assetListSetting = [ + SettingsSwitchListTile( + valueNotifier: showStorageIndicator, + title: 'theme_setting_asset_list_storage_indicator_title'.tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + const LayoutSettings(), + const GroupSettings(), + ]; + + return SettingsSubPageScaffold( + settings: assetListSetting, + showDivider: true, ); } } diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart deleted file mode 100644 index 866e0a08fb..0000000000 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; - -class StorageIndicator extends HookConsumerWidget { - const StorageIndicator({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - - final showStorageIndicator = useState(true); - - void switchChanged(bool value) { - appSettingService.setSetting(AppSettingsEnum.storageIndicator, value); - showStorageIndicator.value = value; - ref.invalidate(appSettingsServiceProvider); - } - - useEffect( - () { - showStorageIndicator.value = appSettingService - .getSetting(AppSettingsEnum.storageIndicator); - - return null; - }, - [], - ); - - return SwitchListTile.adaptive( - activeColor: context.primaryColor, - title: Text( - "theme_setting_asset_list_storage_indicator_title", - style: context.textTheme.labelLarge, - ).tr(), - onChanged: switchChanged, - value: showStorageIndicator.value, - ); - } -} diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart deleted file mode 100644 index 23690f907d..0000000000 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; - -class TilesPerRow extends HookConsumerWidget { - const TilesPerRow({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - - final itemsValue = useState(4.0); - - void sliderChanged(double value) { - appSettingService.setSetting(AppSettingsEnum.tilesPerRow, value.toInt()); - itemsValue.value = value; - ref.invalidate(appSettingsServiceProvider); - } - - useEffect( - () { - int tilesPerRow = - appSettingService.getSetting(AppSettingsEnum.tilesPerRow); - itemsValue.value = tilesPerRow.toDouble(); - return null; - }, - [], - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - title: Text( - "theme_setting_asset_list_tiles_per_row_title", - style: context.textTheme.labelLarge, - ).tr(args: ["${itemsValue.value.toInt()}"]), - ), - Slider( - onChanged: sliderChanged, - value: itemsValue.value, - min: 2, - max: 6, - divisions: 4, - label: "${itemsValue.value.toInt()}", - activeColor: context.primaryColor, - ), - ], - ); - } -} diff --git a/mobile/lib/modules/settings/ui/backup_settings/background_settings.dart b/mobile/lib/modules/settings/ui/backup_settings/background_settings.dart new file mode 100644 index 0000000000..73f7d120c0 --- /dev/null +++ b/mobile/lib/modules/settings/ui/backup_settings/background_settings.dart @@ -0,0 +1,234 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; +import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; +import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_button_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_slider_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class BackgroundBackupSettings extends ConsumerWidget { + const BackgroundBackupSettings({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isBackgroundEnabled = + ref.watch(backupProvider.select((s) => s.backgroundBackup)); + final iosSettings = ref.watch(iOSBackgroundSettingsProvider); + + void showErrorToUser(String msg) { + final snackBar = SnackBar( + content: Text( + msg.tr(), + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + backgroundColor: Colors.red, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + void showBatteryOptimizationInfoToUser() { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext ctx) { + return AlertDialog( + title: const Text( + 'backup_controller_page_background_battery_info_title', + ).tr(), + content: SingleChildScrollView( + child: const Text( + 'backup_controller_page_background_battery_info_message', + ).tr(), + ), + actions: [ + ElevatedButton( + onPressed: () => launchUrl( + Uri.parse('https://dontkillmyapp.com'), + mode: LaunchMode.externalApplication, + ), + child: const Text( + "backup_controller_page_background_battery_info_link", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + ).tr(), + ), + ElevatedButton( + child: const Text( + 'backup_controller_page_background_battery_info_ok', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + ).tr(), + onPressed: () => ctx.pop(), + ), + ], + ); + }, + ); + } + + if (!isBackgroundEnabled) { + return SettingsButtonListTile( + icon: Icons.cloud_sync_outlined, + title: 'backup_controller_page_background_is_off'.tr(), + subtileText: 'backup_controller_page_background_description'.tr(), + buttonText: 'backup_controller_page_background_turn_on'.tr(), + onButtonTap: () => + ref.read(backupProvider.notifier).configureBackgroundBackup( + enabled: true, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, + ), + ); + } + + return Column( + children: [ + if (!Platform.isIOS || iosSettings?.appRefreshEnabled == true) + _BackgroundSettingsEnabled( + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, + ), + if (Platform.isIOS && iosSettings?.appRefreshEnabled != true) + _IOSBackgroundRefreshDisabled(), + if (Platform.isIOS && iosSettings != null) + IosDebugInfoTile(settings: iosSettings), + ], + ); + } +} + +class _IOSBackgroundRefreshDisabled extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SettingsButtonListTile( + icon: Icons.task_outlined, + title: + 'backup_controller_page_background_app_refresh_disabled_title'.tr(), + subtileText: + 'backup_controller_page_background_app_refresh_disabled_content'.tr(), + buttonText: + 'backup_controller_page_background_app_refresh_enable_button_text' + .tr(), + onButtonTap: () => openAppSettings(), + ); + } +} + +class _BackgroundSettingsEnabled extends HookConsumerWidget { + final void Function(String msg) onError; + final void Function() onBatteryInfo; + + const _BackgroundSettingsEnabled({ + required this.onError, + required this.onBatteryInfo, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isWifiRequired = + ref.watch(backupProvider.select((s) => s.backupRequireWifi)); + final isWifiRequiredNotifier = useValueNotifier(isWifiRequired); + useValueChanged( + isWifiRequired, + (_, __) => WidgetsBinding.instance.addPostFrameCallback( + (_) => isWifiRequiredNotifier.value = isWifiRequired, + ), + ); + + final isChargingRequired = + ref.watch(backupProvider.select((s) => s.backupRequireCharging)); + final isChargingRequiredNotifier = useValueNotifier(isChargingRequired); + useValueChanged( + isChargingRequired, + (_, __) => WidgetsBinding.instance.addPostFrameCallback( + (_) => isChargingRequiredNotifier.value = isChargingRequired, + ), + ); + + int backupDelayToSliderValue(int ms) => switch (ms) { + 5000 => 0, + 30000 => 1, + 120000 => 2, + _ => 3, + }; + + int backupDelayToMilliseconds(int v) => + switch (v) { 0 => 5000, 1 => 30000, 2 => 120000, _ => 600000 }; + + String formatBackupDelaySliderValue(int v) => switch (v) { + 0 => 'setting_notifications_notify_seconds'.tr(args: const ['5']), + 1 => 'setting_notifications_notify_seconds'.tr(args: const ['30']), + 2 => 'setting_notifications_notify_minutes'.tr(args: const ['2']), + _ => 'setting_notifications_notify_minutes'.tr(args: const ['10']), + }; + + final backupTriggerDelay = + ref.watch(backupProvider.select((s) => s.backupTriggerDelay)); + final triggerDelay = useState(backupDelayToSliderValue(backupTriggerDelay)); + useValueChanged( + triggerDelay.value, + (_, __) => ref.read(backupProvider.notifier).configureBackgroundBackup( + triggerDelay: backupDelayToMilliseconds(triggerDelay.value), + onError: onError, + onBatteryInfo: onBatteryInfo, + ), + ); + + return SettingsButtonListTile( + icon: Icons.cloud_sync_rounded, + iconColor: context.primaryColor, + title: 'backup_controller_page_background_is_on'.tr(), + buttonText: 'backup_controller_page_background_turn_off'.tr(), + onButtonTap: () => + ref.read(backupProvider.notifier).configureBackgroundBackup( + enabled: false, + onError: onError, + onBatteryInfo: onBatteryInfo, + ), + subtitle: Column( + children: [ + SettingsSwitchListTile( + valueNotifier: isWifiRequiredNotifier, + title: 'backup_controller_page_background_wifi'.tr(), + icon: Icons.wifi, + onChanged: (enabled) => + ref.read(backupProvider.notifier).configureBackgroundBackup( + requireWifi: enabled, + onError: onError, + onBatteryInfo: onBatteryInfo, + ), + ), + SettingsSwitchListTile( + valueNotifier: isChargingRequiredNotifier, + title: 'backup_controller_page_background_charging'.tr(), + icon: Icons.charging_station, + onChanged: (enabled) => + ref.read(backupProvider.notifier).configureBackgroundBackup( + requireCharging: enabled, + onError: onError, + onBatteryInfo: onBatteryInfo, + ), + ), + if (Platform.isAndroid) + SettingsSliderListTile( + valueNotifier: triggerDelay, + text: 'backup_controller_page_background_delay'.tr( + args: [formatBackupDelaySliderValue(triggerDelay.value)], + ), + maxValue: 3.0, + noDivisons: 3, + label: formatBackupDelaySliderValue(triggerDelay.value), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/backup_settings/backup_settings.dart b/mobile/lib/modules/settings/ui/backup_settings/backup_settings.dart new file mode 100644 index 0000000000..e095c8d053 --- /dev/null +++ b/mobile/lib/modules/settings/ui/backup_settings/backup_settings.dart @@ -0,0 +1,68 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/backup/providers/backup_verification.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/backup_settings/background_settings.dart'; +import 'package:immich_mobile/modules/settings/ui/backup_settings/foreground_settings.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_button_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class BackupSettings extends HookConsumerWidget { + const BackupSettings({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ignoreIcloudAssets = + useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); + final isAdvancedTroubleshooting = + useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); + + final backupSettings = [ + const ForegroundBackupSettings(), + const BackgroundBackupSettings(), + if (Platform.isIOS) + SettingsSwitchListTile( + valueNotifier: ignoreIcloudAssets, + title: 'Ignore iCloud photos', + subtitle: + 'Photos that are stored on iCloud will not be uploaded to the Immich server', + ), + if (Platform.isAndroid && isAdvancedTroubleshooting.value) + SettingsButtonListTile( + icon: Icons.warning_rounded, + title: 'Check for corrupt asset backups', + subtitle: isCorruptCheckInProgress + ? const Column( + children: [ + SizedBox(height: 20), + Center(child: ImmichLoadingIndicator()), + SizedBox(height: 20), + ], + ) + : null, + subtileText: !isCorruptCheckInProgress + ? 'Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.' + : null, + buttonText: 'Perform check', + onButtonTap: !isCorruptCheckInProgress + ? () => ref + .read(backupVerificationProvider.notifier) + .performBackupCheck(context) + : null, + ), + ]; + + return SettingsSubPageScaffold( + settings: backupSettings, + showDivider: true, + ); + } +} diff --git a/mobile/lib/modules/settings/ui/backup_settings/foreground_settings.dart b/mobile/lib/modules/settings/ui/backup_settings/foreground_settings.dart new file mode 100644 index 0000000000..684fc95f2b --- /dev/null +++ b/mobile/lib/modules/settings/ui/backup_settings/foreground_settings.dart @@ -0,0 +1,36 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_button_list_tile.dart'; + +class ForegroundBackupSettings extends ConsumerWidget { + const ForegroundBackupSettings({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isAutoBackup = ref.watch(backupProvider.select((s) => s.autoBackup)); + + void onButtonTap() => + ref.read(backupProvider.notifier).setAutoBackup(!isAutoBackup); + + if (isAutoBackup) { + return SettingsButtonListTile( + icon: Icons.cloud_done_rounded, + iconColor: context.primaryColor, + title: 'backup_controller_page_status_on'.tr(), + buttonText: 'backup_controller_page_turn_off'.tr(), + onButtonTap: onButtonTap, + ); + } + + return SettingsButtonListTile( + icon: Icons.cloud_off_rounded, + title: 'backup_controller_page_status_off'.tr(), + subtileText: 'backup_controller_page_desc_backup'.tr(), + buttonText: 'backup_controller_page_turn_on'.tr(), + onButtonTap: onButtonTap, + ); + } +} diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting.dart new file mode 100644 index 0000000000..21753ef544 --- /dev/null +++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting.dart @@ -0,0 +1,41 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; + +class ImageViewerQualitySetting extends HookWidget { + const ImageViewerQualitySetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview); + final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal); + + final viewerSettings = [ + ListTile( + title: Text( + 'setting_image_viewer_help', + style: context.textTheme.bodyMedium, + ).tr(), + ), + SettingsSwitchListTile( + valueNotifier: isPreview, + title: "setting_image_viewer_preview_title".tr(), + subtitle: "setting_image_viewer_preview_subtitle".tr(), + ), + SettingsSwitchListTile( + valueNotifier: isOriginal, + title: "setting_image_viewer_original_title".tr(), + subtitle: "setting_image_viewer_original_subtitle".tr(), + ), + ]; + + return SettingsSubPageScaffold(settings: viewerSettings); + } +} diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart deleted file mode 100644 index bd77043a84..0000000000 --- a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; - -class ImageViewerQualitySetting extends HookConsumerWidget { - const ImageViewerQualitySetting({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(appSettingsServiceProvider); - final isPreview = useState(AppSettingsEnum.loadPreview.defaultValue); - final isOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); - - useEffect( - () { - isPreview.value = settings.getSetting(AppSettingsEnum.loadPreview); - isOriginal.value = settings.getSetting(AppSettingsEnum.loadOriginal); - return null; - }, - ); - - return ExpansionTile( - textColor: context.primaryColor, - title: Text( - 'theme_setting_image_viewer_quality_title', - style: context.textTheme.titleMedium, - ).tr(), - subtitle: const Text( - 'theme_setting_image_viewer_quality_subtitle', - ).tr(), - children: [ - ListTile( - title: Text( - 'setting_image_viewer_help', - style: context.textTheme.bodyMedium, - ).tr(), - ), - SettingsSwitchListTile( - appSettingService: settings, - valueNotifier: isPreview, - settingsEnum: AppSettingsEnum.loadPreview, - title: "setting_image_viewer_preview_title".tr(), - subtitle: "setting_image_viewer_preview_subtitle".tr(), - ), - SettingsSwitchListTile( - appSettingService: settings, - valueNotifier: isOriginal, - settingsEnum: AppSettingsEnum.loadOriginal, - title: "setting_image_viewer_original_title".tr(), - subtitle: "setting_image_viewer_original_subtitle".tr(), - ), - ], - ); - } -} diff --git a/mobile/lib/modules/settings/ui/local_storage_settings.dart b/mobile/lib/modules/settings/ui/local_storage_settings.dart new file mode 100644 index 0000000000..1547ce35ef --- /dev/null +++ b/mobile/lib/modules/settings/ui/local_storage_settings.dart @@ -0,0 +1,54 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; + +class LocalStorageSettings extends HookConsumerWidget { + const LocalStorageSettings({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final isarDb = ref.watch(dbProvider); + final cacheItemCount = useState(0); + + useEffect( + () { + cacheItemCount.value = isarDb.duplicatedAssets.countSync(); + return null; + }, + [], + ); + + void clearCache() async { + await isarDb.writeTxn(() => isarDb.duplicatedAssets.clear()); + cacheItemCount.value = await isarDb.duplicatedAssets.count(); + } + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + dense: true, + title: Text( + "cache_settings_duplicated_assets_title", + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(args: ["${cacheItemCount.value}"]), + subtitle: const Text( + "cache_settings_duplicated_assets_subtitle", + ).tr(), + trailing: TextButton( + onPressed: cacheItemCount.value > 0 ? clearCache : null, + child: Text( + "cache_settings_duplicated_assets_clear_button", + style: TextStyle( + fontSize: 12, + color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart b/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart deleted file mode 100644 index a64da04811..0000000000 --- a/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/shared/providers/db.provider.dart'; - -class LocalStorageSettings extends HookConsumerWidget { - const LocalStorageSettings({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - final isarDb = ref.watch(dbProvider); - final cacheItemCount = useState(0); - useEffect( - () { - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - return null; - }, - [], - ); - - void clearCache() { - isarDb.writeTxnSync(() => isarDb.duplicatedAssets.clearSync()); - cacheItemCount.value = isarDb.duplicatedAssets.countSync(); - } - - return ExpansionTile( - textColor: context.primaryColor, - title: Text( - "cache_settings_tile_title", - style: context.textTheme.titleMedium, - ).tr(), - subtitle: const Text( - "cache_settings_tile_subtitle", - ).tr(), - children: [ - ListTile( - title: Text( - "cache_settings_duplicated_assets_title", - style: context.textTheme.titleSmall, - ).tr(args: ["${cacheItemCount.value}"]), - subtitle: const Text( - "cache_settings_duplicated_assets_subtitle", - ).tr(), - trailing: TextButton( - onPressed: cacheItemCount.value > 0 ? clearCache : null, - child: Text( - "cache_settings_duplicated_assets_clear_button", - style: TextStyle( - fontSize: 12, - color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, - fontWeight: FontWeight.bold, - ), - ).tr(), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/modules/settings/ui/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting.dart new file mode 100644 index 0000000000..0d7f0f5b4f --- /dev/null +++ b/mobile/lib/modules/settings/ui/notification_setting.dart @@ -0,0 +1,118 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_button_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_slider_list_tile.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class NotificationSetting extends HookConsumerWidget { + const NotificationSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final permissionService = ref.watch(notificationPermissionProvider); + + final sliderValue = + useAppSettingsState(AppSettingsEnum.uploadErrorNotificationGracePeriod); + final totalProgressValue = + useAppSettingsState(AppSettingsEnum.backgroundBackupTotalProgress); + final singleProgressValue = + useAppSettingsState(AppSettingsEnum.backgroundBackupSingleProgress); + + final hasPermission = permissionService == PermissionStatus.granted; + + openAppNotificationSettings(BuildContext ctx) { + ctx.pop(); + openAppSettings(); + } + + // When permissions are permanently denied, you need to go to settings to + // allow them + showPermissionsDialog() { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + content: const Text('notification_permission_dialog_content').tr(), + actions: [ + TextButton( + child: const Text('notification_permission_dialog_cancel').tr(), + onPressed: () => ctx.pop(), + ), + TextButton( + onPressed: () => openAppNotificationSettings(ctx), + child: const Text('notification_permission_dialog_settings').tr(), + ), + ], + ), + ); + } + + final String formattedValue = + _formatSliderValue(sliderValue.value.toDouble()); + + final notificationSettings = [ + if (!hasPermission) + SettingsButtonListTile( + icon: Icons.notifications_outlined, + title: 'notification_permission_list_tile_title'.tr(), + subtileText: 'notification_permission_list_tile_content'.tr(), + buttonText: 'notification_permission_list_tile_enable_button'.tr(), + onButtonTap: () => ref + .watch(notificationPermissionProvider.notifier) + .requestNotificationPermission() + .then((permission) { + if (permission == PermissionStatus.permanentlyDenied) { + showPermissionsDialog(); + } + }), + ), + SettingsSwitchListTile( + enabled: hasPermission, + valueNotifier: totalProgressValue, + title: 'setting_notifications_total_progress_title'.tr(), + subtitle: 'setting_notifications_total_progress_subtitle'.tr(), + ), + SettingsSwitchListTile( + enabled: hasPermission, + valueNotifier: singleProgressValue, + title: 'setting_notifications_single_progress_title'.tr(), + subtitle: 'setting_notifications_single_progress_subtitle'.tr(), + ), + SettingsSliderListTile( + enabled: hasPermission, + valueNotifier: sliderValue, + text: 'setting_notifications_notify_failures_grace_period' + .tr(args: [formattedValue]), + maxValue: 5.0, + noDivisons: 5, + label: formattedValue, + ), + ]; + + return SettingsSubPageScaffold(settings: notificationSettings); + } +} + +String _formatSliderValue(double v) { + if (v == 0.0) { + return 'setting_notifications_notify_immediately'.tr(); + } else if (v == 1.0) { + return 'setting_notifications_notify_minutes'.tr(args: const ['30']); + } else if (v == 2.0) { + return 'setting_notifications_notify_hours'.tr(args: const ['2']); + } else if (v == 3.0) { + return 'setting_notifications_notify_hours'.tr(args: const ['8']); + } else if (v == 4.0) { + return 'setting_notifications_notify_hours'.tr(args: const ['24']); + } else { + return 'setting_notifications_notify_never'.tr(); + } +} diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart deleted file mode 100644 index e696c9f1a3..0000000000 --- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; -import 'package:permission_handler/permission_handler.dart'; - -class NotificationSetting extends HookConsumerWidget { - const NotificationSetting({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final appSettingService = ref.watch(appSettingsServiceProvider); - final permissionService = ref.watch(notificationPermissionProvider); - - final sliderValue = useState(0.0); - final totalProgressValue = - useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue); - final singleProgressValue = - useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue); - final hasPermission = permissionService == PermissionStatus.granted; - - useEffect( - () { - sliderValue.value = appSettingService - .getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod) - .toDouble(); - totalProgressValue.value = appSettingService - .getSetting(AppSettingsEnum.backgroundBackupTotalProgress); - singleProgressValue.value = appSettingService - .getSetting(AppSettingsEnum.backgroundBackupSingleProgress); - return null; - }, - [], - ); - - // When permissions are permanently denied, you need to go to settings to - // allow them - showPermissionsDialog() { - showDialog( - context: context, - builder: (context) => AlertDialog( - content: const Text('notification_permission_dialog_content').tr(), - actions: [ - TextButton( - child: const Text('notification_permission_dialog_cancel').tr(), - onPressed: () => context.pop(), - ), - TextButton( - child: const Text('notification_permission_dialog_settings').tr(), - onPressed: () { - context.pop(); - openAppSettings(); - }, - ), - ], - ), - ); - } - - final String formattedValue = _formatSliderValue(sliderValue.value); - return ExpansionTile( - textColor: context.primaryColor, - title: Text( - 'setting_notifications_title', - style: context.textTheme.titleMedium, - ).tr(), - subtitle: const Text( - 'setting_notifications_subtitle', - ).tr(), - children: [ - if (!hasPermission) - ListTile( - leading: const Icon(Icons.notifications_outlined), - title: Text( - 'notification_permission_list_tile_title', - style: context.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), - ).tr(), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'notification_permission_list_tile_content', - style: context.textTheme.labelMedium, - ).tr(), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () => ref - .watch(notificationPermissionProvider.notifier) - .requestNotificationPermission() - .then((permission) { - if (permission == PermissionStatus.permanentlyDenied) { - showPermissionsDialog(); - } - }), - child: const Text( - 'notification_permission_list_tile_enable_button', - ).tr(), - ), - ], - ), - isThreeLine: true, - ), - SettingsSwitchListTile( - enabled: hasPermission, - appSettingService: appSettingService, - valueNotifier: totalProgressValue, - settingsEnum: AppSettingsEnum.backgroundBackupTotalProgress, - title: 'setting_notifications_total_progress_title'.tr(), - subtitle: 'setting_notifications_total_progress_subtitle'.tr(), - ), - SettingsSwitchListTile( - enabled: hasPermission, - appSettingService: appSettingService, - valueNotifier: singleProgressValue, - settingsEnum: AppSettingsEnum.backgroundBackupSingleProgress, - title: 'setting_notifications_single_progress_title'.tr(), - subtitle: 'setting_notifications_single_progress_subtitle'.tr(), - ), - ListTile( - enabled: hasPermission, - isThreeLine: false, - dense: true, - title: const Text( - 'setting_notifications_notify_failures_grace_period', - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(args: [formattedValue]), - subtitle: Slider( - value: sliderValue.value, - onChanged: - !hasPermission ? null : (double v) => sliderValue.value = v, - onChangeEnd: (double v) => appSettingService.setSetting( - AppSettingsEnum.uploadErrorNotificationGracePeriod, - v.toInt(), - ), - max: 5.0, - divisions: 5, - label: formattedValue, - activeColor: context.primaryColor, - ), - ), - ], - ); - } -} - -String _formatSliderValue(double v) { - if (v == 0.0) { - return 'setting_notifications_notify_immediately'.tr(); - } else if (v == 1.0) { - return 'setting_notifications_notify_minutes'.tr(args: const ['30']); - } else if (v == 2.0) { - return 'setting_notifications_notify_hours'.tr(args: const ['2']); - } else if (v == 3.0) { - return 'setting_notifications_notify_hours'.tr(args: const ['8']); - } else if (v == 4.0) { - return 'setting_notifications_notify_hours'.tr(args: const ['24']); - } else { - return 'setting_notifications_notify_never'.tr(); - } -} diff --git a/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart b/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart new file mode 100644 index 0000000000..f75891437c --- /dev/null +++ b/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/settings/ui/preference_settings/theme_setting.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; + +class PreferenceSetting extends StatelessWidget { + const PreferenceSetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + const preferenceSettings = [ + ThemeSetting(), + ]; + + return const SettingsSubPageScaffold(settings: preferenceSettings); + } +} diff --git a/mobile/lib/modules/settings/ui/preference_settings/theme_setting.dart b/mobile/lib/modules/settings/ui/preference_settings/theme_setting.dart new file mode 100644 index 0000000000..3dd023a45a --- /dev/null +++ b/mobile/lib/modules/settings/ui/preference_settings/theme_setting.dart @@ -0,0 +1,81 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; +import 'package:immich_mobile/utils/immich_app_theme.dart'; + +class ThemeSetting extends HookConsumerWidget { + const ThemeSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode); + final currentTheme = useValueNotifier(ref.read(immichThemeProvider)); + final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); + final isSystemTheme = + useValueNotifier(currentTheme.value == ThemeMode.system); + + useValueChanged( + currentThemeString.value, + (_, __) => currentTheme.value = switch (currentThemeString.value) { + "light" => ThemeMode.light, + "dark" => ThemeMode.dark, + _ => ThemeMode.system, + }, + ); + + void onThemeChange(bool isDark) { + if (isDark) { + ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + currentThemeString.value = "dark"; + } else { + ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + currentThemeString.value = "light"; + } + } + + void onSystemThemeChange(bool isSystem) { + if (isSystem) { + currentThemeString.value = "system"; + isSystemTheme.value = true; + ref.watch(immichThemeProvider.notifier).state = ThemeMode.system; + } else { + final currentSystemBrightness = + MediaQuery.platformBrightnessOf(context); + isSystemTheme.value = false; + isDarkTheme.value = currentSystemBrightness == Brightness.dark; + if (currentSystemBrightness == Brightness.light) { + currentThemeString.value = "light"; + ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + } else if (currentSystemBrightness == Brightness.dark) { + currentThemeString.value = "dark"; + ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + } + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "theme_setting_theme_title".tr()), + SettingsSwitchListTile( + valueNotifier: isSystemTheme, + title: 'theme_setting_system_theme_switch'.tr(), + onChanged: onSystemThemeChange, + ), + if (currentTheme.value != ThemeMode.system) + SettingsSwitchListTile( + valueNotifier: isDarkTheme, + title: 'theme_setting_dark_mode_switch'.tr(), + onChanged: onThemeChange, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/settings/ui/settings_button_list_tile.dart b/mobile/lib/modules/settings/ui/settings_button_list_tile.dart new file mode 100644 index 0000000000..fca5b878de --- /dev/null +++ b/mobile/lib/modules/settings/ui/settings_button_list_tile.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingsButtonListTile extends StatelessWidget { + final IconData icon; + final Color? iconColor; + final String title; + final Widget? subtitle; + final String? subtileText; + final String buttonText; + final void Function()? onButtonTap; + + const SettingsButtonListTile({ + required this.icon, + this.iconColor, + required this.title, + this.subtileText, + this.subtitle, + required this.buttonText, + this.onButtonTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + horizontalTitleGap: 20, + isThreeLine: true, + leading: Icon(icon, color: iconColor), + title: Text( + title, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (subtileText != null) const SizedBox(height: 4), + if (subtileText != null) + Text(subtileText!, style: context.textTheme.bodyMedium), + if (subtitle != null) subtitle!, + const SizedBox(height: 6), + ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/settings_radio_list_tile.dart b/mobile/lib/modules/settings/ui/settings_radio_list_tile.dart new file mode 100644 index 0000000000..1c26682a65 --- /dev/null +++ b/mobile/lib/modules/settings/ui/settings_radio_list_tile.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingsRadioGroup { + final String title; + final T value; + + SettingsRadioGroup({required this.title, required this.value}); +} + +class SettingsRadioListTile extends StatelessWidget { + final List groups; + final T groupBy; + final void Function(T?) onRadioChanged; + + const SettingsRadioListTile({ + super.key, + required this.groups, + required this.groupBy, + required this.onRadioChanged, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: groups + .map( + (g) => RadioListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + dense: true, + activeColor: context.primaryColor, + title: Text( + g.title, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + value: g.value, + groupValue: groupBy, + onChanged: onRadioChanged, + controlAffinity: ListTileControlAffinity.trailing, + ), + ) + .toList(), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/settings_slider_list_tile.dart b/mobile/lib/modules/settings/ui/settings_slider_list_tile.dart new file mode 100644 index 0000000000..386a690864 --- /dev/null +++ b/mobile/lib/modules/settings/ui/settings_slider_list_tile.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingsSliderListTile extends StatelessWidget { + final ValueNotifier valueNotifier; + final String text; + final double maxValue; + final double minValue; + final int noDivisons; + final String? label; + final bool enabled; + final Function(int)? onChangeEnd; + + const SettingsSliderListTile({ + required this.valueNotifier, + required this.text, + required this.maxValue, + this.minValue = 0.0, + required this.noDivisons, + this.enabled = true, + this.label, + this.onChangeEnd, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + dense: true, + title: Text( + text, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Slider( + value: valueNotifier.value.toDouble(), + onChanged: (double v) => valueNotifier.value = v.toInt(), + onChangeEnd: (double v) => onChangeEnd?.call(v.toInt()), + max: maxValue, + min: minValue, + divisions: noDivisons, + label: label ?? "${valueNotifier.value}", + activeColor: context.primaryColor, + ), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/settings_sub_page_scaffold.dart b/mobile/lib/modules/settings/ui/settings_sub_page_scaffold.dart new file mode 100644 index 0000000000..96c4678ede --- /dev/null +++ b/mobile/lib/modules/settings/ui/settings_sub_page_scaffold.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class SettingsSubPageScaffold extends StatelessWidget { + final List settings; + final bool showDivider; + + const SettingsSubPageScaffold({ + super.key, + required this.settings, + this.showDivider = false, + }); + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 20), + itemCount: settings.length, + itemBuilder: (ctx, index) => settings[index], + separatorBuilder: (context, index) => showDivider + ? const Column( + children: [ + SizedBox(height: 5), + Divider(height: 10, indent: 15, endIndent: 15), + SizedBox(height: 15), + ], + ) + : const SizedBox(height: 10), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/settings_sub_title.dart b/mobile/lib/modules/settings/ui/settings_sub_title.dart new file mode 100644 index 0000000000..9a3fb6947d --- /dev/null +++ b/mobile/lib/modules/settings/ui/settings_sub_title.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingsSubTitle extends StatelessWidget { + final String title; + + const SettingsSubTitle({ + super.key, + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + title, + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} diff --git a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart index b5277b9c15..c7328f0b96 100644 --- a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart +++ b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart @@ -1,51 +1,61 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; class SettingsSwitchListTile extends StatelessWidget { - final AppSettingsService appSettingService; final ValueNotifier valueNotifier; - final AppSettingsEnum settingsEnum; final String title; final bool enabled; final String? subtitle; + final IconData? icon; final Function(bool)? onChanged; - SettingsSwitchListTile({ - required this.appSettingService, + const SettingsSwitchListTile({ required this.valueNotifier, - required this.settingsEnum, required this.title, this.subtitle, + this.icon, this.enabled = true, this.onChanged, - }) : super(key: Key(settingsEnum.name)); + super.key, + }); @override Widget build(BuildContext context) { + void onSwitchChanged(bool value) { + if (!enabled) return; + + valueNotifier.value = value; + onChanged?.call(value); + } + return SwitchListTile.adaptive( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, - onChanged: (bool value) { - if (enabled) { - valueNotifier.value = value; - appSettingService.setSetting(settingsEnum, value); - } - if (onChanged != null) { - onChanged!(value); - } - }, + onChanged: onSwitchChanged, activeColor: enabled ? context.primaryColor : context.themeData.disabledColor, dense: true, + secondary: icon != null + ? Icon( + icon!, + color: valueNotifier.value ? context.primaryColor : null, + ) + : null, title: Text( title, - style: context.textTheme.titleSmall, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: enabled ? null : context.themeData.disabledColor, + height: 1.5, + ), ), subtitle: subtitle != null ? Text( subtitle!, - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: enabled ? null : context.themeData.disabledColor, + ), ) : null, ); diff --git a/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart b/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart deleted file mode 100644 index e8a1453935..0000000000 --- a/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; - -class ThemeSetting extends HookConsumerWidget { - const ThemeSetting({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentTheme = useState(ThemeMode.system); - - useEffect( - () { - currentTheme.value = ref.read(immichThemeProvider); - return null; - }, - [], - ); - - return ExpansionTile( - textColor: context.primaryColor, - title: Text( - 'theme_setting_theme_title', - style: context.textTheme.titleMedium, - ).tr(), - subtitle: const Text( - 'theme_setting_theme_subtitle', - ).tr(), - children: [ - SwitchListTile.adaptive( - activeColor: context.primaryColor, - title: Text( - 'theme_setting_system_theme_switch', - style: context.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), - ).tr(), - value: currentTheme.value == ThemeMode.system, - onChanged: (bool isSystem) { - var currentSystemBrightness = - MediaQuery.of(context).platformBrightness; - - if (isSystem) { - currentTheme.value = ThemeMode.system; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.system; - ref - .watch(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.themeMode, "system"); - } else { - if (currentSystemBrightness == Brightness.light) { - currentTheme.value = ThemeMode.light; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; - ref - .watch(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.themeMode, "light"); - } else if (currentSystemBrightness == Brightness.dark) { - currentTheme.value = ThemeMode.dark; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; - ref - .watch(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.themeMode, "dark"); - } - } - }, - ), - if (currentTheme.value != ThemeMode.system) - SwitchListTile.adaptive( - activeColor: context.primaryColor, - title: Text( - 'theme_setting_dark_mode_switch', - style: context.textTheme.labelLarge - ?.copyWith(fontWeight: FontWeight.bold), - ).tr(), - value: ref.watch(immichThemeProvider) == ThemeMode.dark, - onChanged: (bool isDark) { - if (isDark) { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; - ref - .watch(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.themeMode, "dark"); - } else { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; - ref - .watch(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.themeMode, "light"); - } - }, - ), - ], - ); - } -} diff --git a/mobile/lib/modules/settings/utils/app_settings_update_hook.dart b/mobile/lib/modules/settings/utils/app_settings_update_hook.dart new file mode 100644 index 0000000000..85ffeda236 --- /dev/null +++ b/mobile/lib/modules/settings/utils/app_settings_update_hook.dart @@ -0,0 +1,18 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/models/store.dart'; + +ValueNotifier useAppSettingsState( + AppSettingsEnum key, +) { + final notifier = useState(Store.get(key.storeKey, key.defaultValue)); + + // Listen to changes to the notifier and update app settings + useValueChanged( + notifier.value, + (_, __) => Store.put(key.storeKey, notifier.value), + ); + + return notifier; +} diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index 3272dd5521..eeb4b379f2 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -1,51 +1,118 @@ 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:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/settings/ui/advanced_settings/advanced_settings.dart'; +import 'package:immich_mobile/modules/settings/ui/advanced_settings.dart'; import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart'; -import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart'; -import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart'; -import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart'; -import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart'; +import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart'; +import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting.dart'; +import 'package:immich_mobile/modules/settings/ui/notification_setting.dart'; +import 'package:immich_mobile/modules/settings/ui/preference_settings/preference_setting.dart'; +import 'package:immich_mobile/routing/router.dart'; + +enum SettingSection { + notifications( + 'setting_notifications_title', + Icons.notifications_none_rounded, + ), + preferences('preferences_settings_title', Icons.interests_outlined), + backup('backup_controller_page_backup', Icons.cloud_upload_outlined), + timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined), + viewer('asset_viewer_settings_title', Icons.image_outlined), + advanced('advanced_settings_tile_title', Icons.build_outlined); + + final String title; + final IconData icon; + + Widget get widget => switch (this) { + SettingSection.notifications => const NotificationSetting(), + SettingSection.preferences => const PreferenceSetting(), + SettingSection.backup => const BackupSettings(), + SettingSection.timeline => const AssetListSettings(), + SettingSection.viewer => const ImageViewerQualitySetting(), + SettingSection.advanced => const AdvancedSettings(), + }; + + const SettingSection(this.title, this.icon); +} @RoutePage() -class SettingsPage extends HookConsumerWidget { +class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - leading: IconButton( - iconSize: 20, - splashRadius: 24, - onPressed: () => context.pop(), - icon: const Icon(Icons.arrow_back_ios_new_rounded), - ), - automaticallyImplyLeading: false, centerTitle: false, - title: const Text( - 'setting_pages_app_bar_settings', - ).tr(), - ), - body: ListView( - children: [ - ...ListTile.divideTiles( - context: context, - tiles: [ - const ImageViewerQualitySetting(), - const ThemeSetting(), - const AssetListSettings(), - const NotificationSetting(), - // const ExperimentalSettings(), - const LocalStorageSettings(), - const AdvancedSettings(), - ], - ), - ], + bottom: const PreferredSize( + preferredSize: Size.fromHeight(1), + child: Divider(height: 1), + ), + title: const Text('setting_pages_app_bar_settings').tr(), ), + body: context.isMobile ? _MobileLayout() : _TabletLayout(), + ); + } +} + +class _MobileLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return ListView( + children: SettingSection.values + .map( + (s) => ListTile( + title: Text( + s.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + leading: Icon(s.icon), + onTap: () => context.pushRoute(SettingsSubRoute(section: s)), + ), + ) + .toList(), + ); + } +} + +class _TabletLayout extends HookWidget { + @override + Widget build(BuildContext context) { + final selectedSection = + useState(SettingSection.values.first); + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: CustomScrollView( + slivers: SettingSection.values + .map( + (s) => SliverToBoxAdapter( + child: ListTile( + title: Text(s.title).tr(), + leading: Icon(s.icon), + selected: s.index == selectedSection.value.index, + selectedColor: context.primaryColor, + selectedTileColor: context.primaryColor.withAlpha(50), + onTap: () => selectedSection.value = s, + ), + ), + ) + .toList(), + ), + ), + const VerticalDivider(width: 1), + Expanded( + flex: 4, + child: selectedSection.value.widget, + ), + ], ); } } diff --git a/mobile/lib/modules/settings/views/settings_sub_page.dart b/mobile/lib/modules/settings/views/settings_sub_page.dart new file mode 100644 index 0000000000..582f45a11c --- /dev/null +++ b/mobile/lib/modules/settings/views/settings_sub_page.dart @@ -0,0 +1,22 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/settings/views/settings_page.dart'; + +@RoutePage() +class SettingsSubPage extends StatelessWidget { + const SettingsSubPage(this.section, {super.key}); + + final SettingSection section; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + title: Text(section.title).tr(), + ), + body: section.widget, + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 6da526b9b5..f5c1a95d9e 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -31,6 +31,7 @@ import 'package:immich_mobile/modules/login/views/change_password_page.dart'; import 'package:immich_mobile/modules/login/views/login_page.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart'; +import 'package:immich_mobile/modules/settings/views/settings_sub_page.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart'; import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart'; @@ -179,6 +180,7 @@ class AppRouter extends _$AppRouter { transitionsBuilder: TransitionsBuilders.slideBottom, ), AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]), + AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 64bd492a77..cc86b701a4 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -299,6 +299,16 @@ abstract class _$AppRouter extends RootStackRouter { child: const SettingsPage(), ); }, + SettingsSubRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: SettingsSubPage( + args.section, + key: args.key, + ), + ); + }, SharedLinkEditRoute.name: (routeData) { final args = routeData.argsAs( orElse: () => const SharedLinkEditRouteArgs()); @@ -1260,6 +1270,44 @@ class SettingsRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [SettingsSubPage] +class SettingsSubRoute extends PageRouteInfo { + SettingsSubRoute({ + required SettingSection section, + Key? key, + List? children, + }) : super( + SettingsSubRoute.name, + args: SettingsSubRouteArgs( + section: section, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'SettingsSubRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class SettingsSubRouteArgs { + const SettingsSubRouteArgs({ + required this.section, + this.key, + }); + + final SettingSection section; + + final Key? key; + + @override + String toString() { + return 'SettingsSubRouteArgs{section: $section, key: $key}'; + } +} + /// generated route for /// [SharedLinkEditPage] class SharedLinkEditRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/ui/immich_toast.dart b/mobile/lib/shared/ui/immich_toast.dart index 25a0e65fa5..e15623c86c 100644 --- a/mobile/lib/shared/ui/immich_toast.dart +++ b/mobile/lib/shared/ui/immich_toast.dart @@ -9,7 +9,7 @@ class ImmichToast { required BuildContext context, required String msg, ToastType toastType = ToastType.info, - ToastGravity gravity = ToastGravity.TOP, + ToastGravity gravity = ToastGravity.BOTTOM, int durationInSecond = 3, }) { final fToast = FToast();