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:
shenlong 2024-03-12 14:56:08 +00:00 committed by GitHub
parent 4733de25af
commit 7489db9481
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1389 additions and 1265 deletions

View 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);
}
}

View file

@ -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();
},
),
],
);
}
}

View file

@ -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,
),
],
);
}
}

View file

@ -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),
),
],
);

View file

@ -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,
);
}
}

View file

@ -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,
);
}
}

View file

@ -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,
),
],
);
}
}

View file

@ -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),
),
],
),
);
}
}

View file

@ -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,
);
}
}

View file

@ -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,
);
}
}

View file

@ -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);
}
}

View file

@ -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(),
),
],
);
}
}

View 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(),
),
);
}
}

View file

@ -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(),
),
),
],
);
}
}

View 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();
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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,
),
],
);
}
}

View file

@ -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)),
],
),
);
}
}

View 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(),
);
}
}

View file

@ -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,
),
);
}
}

View file

@ -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),
);
}
}

View 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,
),
),
);
}
}

View file

@ -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,
);

View file

@ -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");
}
},
),
],
);
}
}