mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
refactor(mobile): app settings (#7749)
* refactor(mobile): app settings * Font size * refactor(mobile): backup settings ui (#7771) * refactor: SettingsButtonListTile * refactor: Backup settings to App settings --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * fix: invalidate appsettingsprovider on timeline setting change * styling --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
4733de25af
commit
7489db9481
@ -43,7 +43,11 @@
|
|||||||
"asset_list_layout_settings_group_by_month": "Month",
|
"asset_list_layout_settings_group_by_month": "Month",
|
||||||
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
"asset_list_layout_settings_group_by_month_day": "Month + day",
|
||||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
"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_device": "Albums on device ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
|
@ -7,7 +7,7 @@ part of 'asset_people.provider.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$assetPeopleNotifierHash() =>
|
String _$assetPeopleNotifierHash() =>
|
||||||
r'192a4ee188f781000fe43f1675c49e1081ccc631';
|
r'9835b180984a750c91e923e7b64dbda94f6d7574';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@ -34,7 +34,7 @@ abstract class _$AssetPeopleNotifier extends BuildlessAutoDisposeAsyncNotifier<
|
|||||||
List<PersonWithFacesResponseDto>> {
|
List<PersonWithFacesResponseDto>> {
|
||||||
late final Asset asset;
|
late final Asset asset;
|
||||||
|
|
||||||
Future<List<PersonWithFacesResponseDto>> build(
|
FutureOr<List<PersonWithFacesResponseDto>> build(
|
||||||
Asset asset,
|
Asset asset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ class AssetPeopleNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
|||||||
final Asset asset;
|
final Asset asset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<PersonWithFacesResponseDto>> runNotifierBuild(
|
FutureOr<List<PersonWithFacesResponseDto>> runNotifierBuild(
|
||||||
covariant AssetPeopleNotifier notifier,
|
covariant AssetPeopleNotifier notifier,
|
||||||
) {
|
) {
|
||||||
return notifier.build(
|
return notifier.build(
|
||||||
|
@ -7,7 +7,7 @@ part of 'video_player_controller_provider.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$videoPlayerControllerHash() =>
|
String _$videoPlayerControllerHash() =>
|
||||||
r'72b45de66542021717807655e25ec92d78d80eec';
|
r'40b31f7b1a73fab84c311b0f06bedf5322143cd9';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
@ -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<void> _performDeletion(
|
||||||
|
BuildContext context,
|
||||||
|
List<Asset> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
mobile/lib/modules/backup/providers/backup_verification.provider.g.dart
generated
Normal file
27
mobile/lib/modules/backup/providers/backup_verification.provider.g.dart
generated
Normal file
@ -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<BackupVerification, bool>.internal(
|
||||||
|
BackupVerification.new,
|
||||||
|
name: r'backupVerificationProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$backupVerificationHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$BackupVerification = AutoDisposeNotifier<bool>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
@ -1,487 +1,12 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.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';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class BackupOptionsPage extends HookConsumerWidget {
|
class BackupOptionsPage extends StatelessWidget {
|
||||||
const BackupOptionsPage({super.key});
|
const BackupOptionsPage({super.key});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
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<void> performDeletion(List<Asset> 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<void>(
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@ -496,26 +21,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: const BackupSettings(),
|
||||||
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(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
mobile/lib/modules/settings/ui/advanced_settings.dart
Normal file
69
mobile/lib/modules/settings/ui/advanced_settings.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<bool>(
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/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/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.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 {
|
class LayoutSettings extends HookConsumerWidget {
|
||||||
const LayoutSettings({
|
const LayoutSettings({
|
||||||
@ -14,96 +15,27 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout);
|
||||||
|
final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow);
|
||||||
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<bool>(AppSettingsEnum.dynamicLayout);
|
|
||||||
groupBy.value = GroupAssetsBy.values[
|
|
||||||
appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile.adaptive(
|
SettingsSubTitle(title: "asset_list_layout_sub_title".tr()),
|
||||||
activeColor: context.primaryColor,
|
SettingsSwitchListTile(
|
||||||
title: Text(
|
valueNotifier: useDynamicLayout,
|
||||||
"asset_list_layout_settings_dynamic_layout_title",
|
title: "asset_list_layout_settings_dynamic_layout_title".tr(),
|
||||||
style: context.textTheme.labelLarge,
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
).tr(),
|
|
||||||
onChanged: switchChanged,
|
|
||||||
value: useDynamicLayout.value,
|
|
||||||
),
|
),
|
||||||
const Divider(
|
SettingsSliderListTile(
|
||||||
indent: 18,
|
valueNotifier: tilesPerRow,
|
||||||
endIndent: 18,
|
text: 'theme_setting_asset_list_tiles_per_row_title'
|
||||||
),
|
.tr(args: ["${tilesPerRow.value}"]),
|
||||||
ListTile(
|
label: "${tilesPerRow.value}",
|
||||||
title: const Text(
|
maxValue: 6,
|
||||||
"asset_list_layout_settings_group_by",
|
minValue: 2,
|
||||||
style: TextStyle(
|
noDivisons: 4,
|
||||||
fontSize: 16,
|
onChangeEnd: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -1,31 +1,37 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'asset_list_tiles_per_row.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({
|
const AssetListSettings({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ExpansionTile(
|
final showStorageIndicator =
|
||||||
textColor: context.primaryColor,
|
useAppSettingsState(AppSettingsEnum.storageIndicator);
|
||||||
title: Text(
|
|
||||||
'asset_list_settings_title',
|
final assetListSetting = [
|
||||||
style: context.textTheme.titleMedium,
|
SettingsSwitchListTile(
|
||||||
).tr(),
|
valueNotifier: showStorageIndicator,
|
||||||
subtitle: const Text(
|
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
||||||
'asset_list_settings_subtitle',
|
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||||
).tr(),
|
),
|
||||||
children: const [
|
const LayoutSettings(),
|
||||||
TilesPerRow(),
|
const GroupSettings(),
|
||||||
StorageIndicator(),
|
];
|
||||||
LayoutSettings(),
|
|
||||||
],
|
return SettingsSubPageScaffold(
|
||||||
|
settings: assetListSetting,
|
||||||
|
showDivider: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<bool>(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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<void>(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
54
mobile/lib/modules/settings/ui/local_storage_settings.dart
Normal file
54
mobile/lib/modules/settings/ui/local_storage_settings.dart
Normal file
@ -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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
118
mobile/lib/modules/settings/ui/notification_setting.dart
Normal file
118
mobile/lib/modules/settings/ui/notification_setting.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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<int>(AppSettingsEnum.uploadErrorNotificationGracePeriod)
|
|
||||||
.toDouble();
|
|
||||||
totalProgressValue.value = appSettingService
|
|
||||||
.getSetting<bool>(AppSettingsEnum.backgroundBackupTotalProgress);
|
|
||||||
singleProgressValue.value = appSettingService
|
|
||||||
.getSetting<bool>(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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
mobile/lib/modules/settings/ui/settings_radio_list_tile.dart
Normal file
47
mobile/lib/modules/settings/ui/settings_radio_list_tile.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class SettingsRadioGroup<T> {
|
||||||
|
final String title;
|
||||||
|
final T value;
|
||||||
|
|
||||||
|
SettingsRadioGroup({required this.title, required this.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsRadioListTile<T> extends StatelessWidget {
|
||||||
|
final List<SettingsRadioGroup> 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<T>(
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class SettingsSliderListTile extends StatelessWidget {
|
||||||
|
final ValueNotifier<int> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SettingsSubPageScaffold extends StatelessWidget {
|
||||||
|
final List<Widget> 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
25
mobile/lib/modules/settings/ui/settings_sub_title.dart
Normal file
25
mobile/lib/modules/settings/ui/settings_sub_title.dart
Normal file
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,51 +1,61 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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 {
|
class SettingsSwitchListTile extends StatelessWidget {
|
||||||
final AppSettingsService appSettingService;
|
|
||||||
final ValueNotifier<bool> valueNotifier;
|
final ValueNotifier<bool> valueNotifier;
|
||||||
final AppSettingsEnum settingsEnum;
|
|
||||||
final String title;
|
final String title;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
final IconData? icon;
|
||||||
final Function(bool)? onChanged;
|
final Function(bool)? onChanged;
|
||||||
|
|
||||||
SettingsSwitchListTile({
|
const SettingsSwitchListTile({
|
||||||
required this.appSettingService,
|
|
||||||
required this.valueNotifier,
|
required this.valueNotifier,
|
||||||
required this.settingsEnum,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
|
this.icon,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
}) : super(key: Key(settingsEnum.name));
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
void onSwitchChanged(bool value) {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
valueNotifier.value = value;
|
||||||
|
onChanged?.call(value);
|
||||||
|
}
|
||||||
|
|
||||||
return SwitchListTile.adaptive(
|
return SwitchListTile.adaptive(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
selectedTileColor: enabled ? null : context.themeData.disabledColor,
|
selectedTileColor: enabled ? null : context.themeData.disabledColor,
|
||||||
value: valueNotifier.value,
|
value: valueNotifier.value,
|
||||||
onChanged: (bool value) {
|
onChanged: onSwitchChanged,
|
||||||
if (enabled) {
|
|
||||||
valueNotifier.value = value;
|
|
||||||
appSettingService.setSetting(settingsEnum, value);
|
|
||||||
}
|
|
||||||
if (onChanged != null) {
|
|
||||||
onChanged!(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activeColor:
|
activeColor:
|
||||||
enabled ? context.primaryColor : context.themeData.disabledColor,
|
enabled ? context.primaryColor : context.themeData.disabledColor,
|
||||||
dense: true,
|
dense: true,
|
||||||
|
secondary: icon != null
|
||||||
|
? Icon(
|
||||||
|
icon!,
|
||||||
|
color: valueNotifier.value ? context.primaryColor : null,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: Text(
|
title: Text(
|
||||||
title,
|
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
|
subtitle: subtitle != null
|
||||||
? Text(
|
? Text(
|
||||||
subtitle!,
|
subtitle!,
|
||||||
style: context.textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: enabled ? null : context.themeData.disabledColor,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: 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>(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");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<T> useAppSettingsState<T>(
|
||||||
|
AppSettingsEnum<T> key,
|
||||||
|
) {
|
||||||
|
final notifier = useState<T>(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;
|
||||||
|
}
|
@ -1,51 +1,118 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package: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/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/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/backup_settings/backup_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/image_viewer_quality_setting.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/notification_setting.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_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()
|
@RoutePage()
|
||||||
class SettingsPage extends HookConsumerWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
|
||||||
iconSize: 20,
|
|
||||||
splashRadius: 24,
|
|
||||||
onPressed: () => context.pop(),
|
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
|
||||||
),
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: const Text(
|
bottom: const PreferredSize(
|
||||||
'setting_pages_app_bar_settings',
|
preferredSize: Size.fromHeight(1),
|
||||||
).tr(),
|
child: Divider(height: 1),
|
||||||
),
|
),
|
||||||
body: ListView(
|
title: const Text('setting_pages_app_bar_settings').tr(),
|
||||||
children: [
|
|
||||||
...ListTile.divideTiles(
|
|
||||||
context: context,
|
|
||||||
tiles: [
|
|
||||||
const ImageViewerQualitySetting(),
|
|
||||||
const ThemeSetting(),
|
|
||||||
const AssetListSettings(),
|
|
||||||
const NotificationSetting(),
|
|
||||||
// const ExperimentalSettings(),
|
|
||||||
const LocalStorageSettings(),
|
|
||||||
const AdvancedSettings(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
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>(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,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
mobile/lib/modules/settings/views/settings_sub_page.dart
Normal file
22
mobile/lib/modules/settings/views/settings_sub_page.dart
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.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/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/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_edit_page.dart';
|
||||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_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,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
),
|
),
|
||||||
AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]),
|
||||||
|
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
||||||
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
|
@ -299,6 +299,16 @@ abstract class _$AppRouter extends RootStackRouter {
|
|||||||
child: const SettingsPage(),
|
child: const SettingsPage(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
SettingsSubRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<SettingsSubRouteArgs>();
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: SettingsSubPage(
|
||||||
|
args.section,
|
||||||
|
key: args.key,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
SharedLinkEditRoute.name: (routeData) {
|
SharedLinkEditRoute.name: (routeData) {
|
||||||
final args = routeData.argsAs<SharedLinkEditRouteArgs>(
|
final args = routeData.argsAs<SharedLinkEditRouteArgs>(
|
||||||
orElse: () => const SharedLinkEditRouteArgs());
|
orElse: () => const SharedLinkEditRouteArgs());
|
||||||
@ -1260,6 +1270,44 @@ class SettingsRoute extends PageRouteInfo<void> {
|
|||||||
static const PageInfo<void> page = PageInfo<void>(name);
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [SettingsSubPage]
|
||||||
|
class SettingsSubRoute extends PageRouteInfo<SettingsSubRouteArgs> {
|
||||||
|
SettingsSubRoute({
|
||||||
|
required SettingSection section,
|
||||||
|
Key? key,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
SettingsSubRoute.name,
|
||||||
|
args: SettingsSubRouteArgs(
|
||||||
|
section: section,
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'SettingsSubRoute';
|
||||||
|
|
||||||
|
static const PageInfo<SettingsSubRouteArgs> page =
|
||||||
|
PageInfo<SettingsSubRouteArgs>(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
|
/// generated route for
|
||||||
/// [SharedLinkEditPage]
|
/// [SharedLinkEditPage]
|
||||||
class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> {
|
class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> {
|
||||||
|
@ -9,7 +9,7 @@ class ImmichToast {
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String msg,
|
required String msg,
|
||||||
ToastType toastType = ToastType.info,
|
ToastType toastType = ToastType.info,
|
||||||
ToastGravity gravity = ToastGravity.TOP,
|
ToastGravity gravity = ToastGravity.BOTTOM,
|
||||||
int durationInSecond = 3,
|
int durationInSecond = 3,
|
||||||
}) {
|
}) {
|
||||||
final fToast = FToast();
|
final fToast = FToast();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user