mirror of
https://github.com/immich-app/immich.git
synced 2024-12-30 11:28:22 +02: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_day": "Month + day",
|
||||
"asset_list_settings_subtitle": "Photo grid layout settings",
|
||||
"asset_list_settings_title": "Photo Grid",
|
||||
"asset_list_settings_title": "Timeline",
|
||||
"asset_list_group_by_sub_title": "Group by",
|
||||
"asset_list_layout_sub_title": "Layout",
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"preferences_settings_title": "Preferences",
|
||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||
|
Binary file not shown.
Binary file not shown.
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
BIN
mobile/lib/modules/backup/providers/backup_verification.provider.g.dart
generated
Normal file
BIN
mobile/lib/modules/backup/providers/backup_verification.provider.g.dart
generated
Normal file
Binary file not shown.
@ -1,487 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart';
|
||||
|
||||
@RoutePage()
|
||||
class BackupOptionsPage extends HookConsumerWidget {
|
||||
class BackupOptionsPage extends StatelessWidget {
|
||||
const BackupOptionsPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
BackUpState backupState = ref.watch(backupProvider);
|
||||
final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings;
|
||||
final settingsService = ref.watch(appSettingsServiceProvider);
|
||||
final showBackupFix = Platform.isAndroid &&
|
||||
settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting);
|
||||
final ignoreIcloudAssets = useState(
|
||||
settingsService.getSetting(AppSettingsEnum.ignoreIcloudAssets),
|
||||
);
|
||||
final appRefreshDisabled =
|
||||
Platform.isIOS && settings?.appRefreshEnabled != true;
|
||||
final checkInProgress = useState(false);
|
||||
|
||||
Future<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,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
@ -496,26 +21,7 @@ class BackupOptionsPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 32.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
buildAutoBackupController(),
|
||||
const Divider(),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Platform.isIOS
|
||||
? (appRefreshDisabled
|
||||
? buildBackgroundAppRefreshWarning()
|
||||
: buildBackgroundBackupController())
|
||||
: buildBackgroundBackupController(),
|
||||
),
|
||||
if (Platform.isIOS) ...buildIgnoreIcloudAssetSetting(),
|
||||
if (showBackupFix) const Divider(),
|
||||
if (showBackupFix) buildCheckCorruptBackups(),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const BackupSettings(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
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:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_slider_list_tile.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart';
|
||||
|
||||
class LayoutSettings extends HookConsumerWidget {
|
||||
const LayoutSettings({
|
||||
@ -14,96 +15,27 @@ class LayoutSettings extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||
|
||||
final useDynamicLayout = useState(true);
|
||||
final groupBy = useState(GroupAssetsBy.day);
|
||||
|
||||
void switchChanged(bool value) {
|
||||
appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value);
|
||||
useDynamicLayout.value = value;
|
||||
ref.invalidate(appSettingsServiceProvider);
|
||||
}
|
||||
|
||||
void changeGroupValue(GroupAssetsBy? value) {
|
||||
if (value != null) {
|
||||
appSettingService.setSetting(
|
||||
AppSettingsEnum.groupAssetsBy,
|
||||
value.index,
|
||||
);
|
||||
groupBy.value = value;
|
||||
ref.invalidate(appSettingsServiceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
useDynamicLayout.value =
|
||||
appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout);
|
||||
groupBy.value = GroupAssetsBy.values[
|
||||
appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
|
||||
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout);
|
||||
final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
activeColor: context.primaryColor,
|
||||
title: Text(
|
||||
"asset_list_layout_settings_dynamic_layout_title",
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
onChanged: switchChanged,
|
||||
value: useDynamicLayout.value,
|
||||
SettingsSubTitle(title: "asset_list_layout_sub_title".tr()),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useDynamicLayout,
|
||||
title: "asset_list_layout_settings_dynamic_layout_title".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
const Divider(
|
||||
indent: 18,
|
||||
endIndent: 18,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
"asset_list_layout_settings_group_by",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
RadioListTile(
|
||||
activeColor: context.primaryColor,
|
||||
title: Text(
|
||||
"asset_list_layout_settings_group_by_month_day",
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
value: GroupAssetsBy.day,
|
||||
groupValue: groupBy.value,
|
||||
onChanged: changeGroupValue,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
),
|
||||
RadioListTile(
|
||||
activeColor: context.primaryColor,
|
||||
title: Text(
|
||||
"asset_list_layout_settings_group_by_month",
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
value: GroupAssetsBy.month,
|
||||
groupValue: groupBy.value,
|
||||
onChanged: changeGroupValue,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
),
|
||||
RadioListTile(
|
||||
activeColor: context.primaryColor,
|
||||
title: Text(
|
||||
"asset_list_layout_settings_group_automatically",
|
||||
style: context.textTheme.labelLarge,
|
||||
).tr(),
|
||||
value: GroupAssetsBy.auto,
|
||||
groupValue: groupBy.value,
|
||||
onChanged: changeGroupValue,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
SettingsSliderListTile(
|
||||
valueNotifier: tilesPerRow,
|
||||
text: 'theme_setting_asset_list_tiles_per_row_title'
|
||||
.tr(args: ["${tilesPerRow.value}"]),
|
||||
label: "${tilesPerRow.value}",
|
||||
maxValue: 6,
|
||||
minValue: 2,
|
||||
noDivisons: 4,
|
||||
onChangeEnd: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,31 +1,37 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart';
|
||||
import 'asset_list_tiles_per_row.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_group_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart';
|
||||
import 'asset_list_layout_settings.dart';
|
||||
|
||||
class AssetListSettings extends StatelessWidget {
|
||||
class AssetListSettings extends HookConsumerWidget {
|
||||
const AssetListSettings({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpansionTile(
|
||||
textColor: context.primaryColor,
|
||||
title: Text(
|
||||
'asset_list_settings_title',
|
||||
style: context.textTheme.titleMedium,
|
||||
).tr(),
|
||||
subtitle: const Text(
|
||||
'asset_list_settings_subtitle',
|
||||
).tr(),
|
||||
children: const [
|
||||
TilesPerRow(),
|
||||
StorageIndicator(),
|
||||
LayoutSettings(),
|
||||
],
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final showStorageIndicator =
|
||||
useAppSettingsState(AppSettingsEnum.storageIndicator);
|
||||
|
||||
final assetListSetting = [
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: showStorageIndicator,
|
||||
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
const LayoutSettings(),
|
||||
const GroupSettings(),
|
||||
];
|
||||
|
||||
return SettingsSubPageScaffold(
|
||||
settings: assetListSetting,
|
||||
showDivider: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
|
||||
class SettingsSwitchListTile extends StatelessWidget {
|
||||
final AppSettingsService appSettingService;
|
||||
final ValueNotifier<bool> valueNotifier;
|
||||
final AppSettingsEnum settingsEnum;
|
||||
final String title;
|
||||
final bool enabled;
|
||||
final String? subtitle;
|
||||
final IconData? icon;
|
||||
final Function(bool)? onChanged;
|
||||
|
||||
SettingsSwitchListTile({
|
||||
required this.appSettingService,
|
||||
const SettingsSwitchListTile({
|
||||
required this.valueNotifier,
|
||||
required this.settingsEnum,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.enabled = true,
|
||||
this.onChanged,
|
||||
}) : super(key: Key(settingsEnum.name));
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void onSwitchChanged(bool value) {
|
||||
if (!enabled) return;
|
||||
|
||||
valueNotifier.value = value;
|
||||
onChanged?.call(value);
|
||||
}
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
selectedTileColor: enabled ? null : context.themeData.disabledColor,
|
||||
value: valueNotifier.value,
|
||||
onChanged: (bool value) {
|
||||
if (enabled) {
|
||||
valueNotifier.value = value;
|
||||
appSettingService.setSetting(settingsEnum, value);
|
||||
}
|
||||
if (onChanged != null) {
|
||||
onChanged!(value);
|
||||
}
|
||||
},
|
||||
onChanged: onSwitchChanged,
|
||||
activeColor:
|
||||
enabled ? context.primaryColor : context.themeData.disabledColor,
|
||||
dense: true,
|
||||
secondary: icon != null
|
||||
? Icon(
|
||||
icon!,
|
||||
color: valueNotifier.value ? context.primaryColor : null,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
title,
|
||||
style: context.textTheme.titleSmall,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: enabled ? null : context.themeData.disabledColor,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle!,
|
||||
style: context.textTheme.bodyMedium,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: enabled ? null : context.themeData.disabledColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
@ -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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/advanced_settings/advanced_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/advanced_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/backup_settings/backup_settings.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/notification_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/preference_settings/preference_setting.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
enum SettingSection {
|
||||
notifications(
|
||||
'setting_notifications_title',
|
||||
Icons.notifications_none_rounded,
|
||||
),
|
||||
preferences('preferences_settings_title', Icons.interests_outlined),
|
||||
backup('backup_controller_page_backup', Icons.cloud_upload_outlined),
|
||||
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined),
|
||||
viewer('asset_viewer_settings_title', Icons.image_outlined),
|
||||
advanced('advanced_settings_tile_title', Icons.build_outlined);
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
|
||||
Widget get widget => switch (this) {
|
||||
SettingSection.notifications => const NotificationSetting(),
|
||||
SettingSection.preferences => const PreferenceSetting(),
|
||||
SettingSection.backup => const BackupSettings(),
|
||||
SettingSection.timeline => const AssetListSettings(),
|
||||
SettingSection.viewer => const ImageViewerQualitySetting(),
|
||||
SettingSection.advanced => const AdvancedSettings(),
|
||||
};
|
||||
|
||||
const SettingSection(this.title, this.icon);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
iconSize: 20,
|
||||
splashRadius: 24,
|
||||
onPressed: () => context.pop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: const Text(
|
||||
'setting_pages_app_bar_settings',
|
||||
).tr(),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
...ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: [
|
||||
const ImageViewerQualitySetting(),
|
||||
const ThemeSetting(),
|
||||
const AssetListSettings(),
|
||||
const NotificationSetting(),
|
||||
// const ExperimentalSettings(),
|
||||
const LocalStorageSettings(),
|
||||
const AdvancedSettings(),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(height: 1),
|
||||
),
|
||||
title: const Text('setting_pages_app_bar_settings').tr(),
|
||||
),
|
||||
body: context.isMobile ? _MobileLayout() : _TabletLayout(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileLayout extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: SettingSection.values
|
||||
.map(
|
||||
(s) => ListTile(
|
||||
title: Text(
|
||||
s.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
leading: Icon(s.icon),
|
||||
onTap: () => context.pushRoute(SettingsSubRoute(section: s)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabletLayout extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedSection =
|
||||
useState<SettingSection>(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/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/views/permission_onboarding_page.dart';
|
||||
import 'package:immich_mobile/modules/settings/views/settings_sub_page.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_edit_page.dart';
|
||||
import 'package:immich_mobile/modules/shared_link/views/shared_link_page.dart';
|
||||
@ -179,6 +180,7 @@ class AppRouter extends _$AppRouter {
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
),
|
||||
AutoRoute(page: SettingsRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]),
|
||||
AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
|
@ -299,6 +299,16 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
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) {
|
||||
final args = routeData.argsAs<SharedLinkEditRouteArgs>(
|
||||
orElse: () => const SharedLinkEditRouteArgs());
|
||||
@ -1260,6 +1270,44 @@ class SettingsRoute extends PageRouteInfo<void> {
|
||||
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
|
||||
/// [SharedLinkEditPage]
|
||||
class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> {
|
||||
|
@ -9,7 +9,7 @@ class ImmichToast {
|
||||
required BuildContext context,
|
||||
required String msg,
|
||||
ToastType toastType = ToastType.info,
|
||||
ToastGravity gravity = ToastGravity.TOP,
|
||||
ToastGravity gravity = ToastGravity.BOTTOM,
|
||||
int durationInSecond = 3,
|
||||
}) {
|
||||
final fToast = FToast();
|
||||
|
Loading…
Reference in New Issue
Block a user