refactor(mobile): widgets (#9291)

* refactor(mobile): widgets

* update
This commit is contained in:
Alex 2024-05-06 23:04:21 -05:00 committed by GitHub
parent 7520ffd6c3
commit 5806a3ce25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
203 changed files with 318 additions and 318 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/widgets/settings/local_storage_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/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

@ -0,0 +1,57 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/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.watch(appSettingsServiceProvider).setSetting(
AppSettingsEnum.groupAssetsBy,
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

@ -0,0 +1,43 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
class LayoutSettings extends HookConsumerWidget {
const LayoutSettings({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final useDynamicLayout = useAppSettingsState(AppSettingsEnum.dynamicLayout);
final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsSubTitle(title: "asset_list_layout_sub_title".tr()),
SettingsSwitchListTile(
valueNotifier: useDynamicLayout,
title: "asset_list_layout_settings_dynamic_layout_title".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
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

@ -0,0 +1,37 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
import 'asset_list_layout_settings.dart';
class AssetListSettings extends HookConsumerWidget {
const AssetListSettings({
super.key,
});
@override
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

@ -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/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
import 'package:immich_mobile/widgets/backup/ios_debug_info_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/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/providers/backup/backup_verification.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/common/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/providers/backup/backup.provider.dart';
import 'package:immich_mobile/widgets/settings/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/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/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

@ -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/constants/locales.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/localization.service.dart';
class LanguageSettings extends HookConsumerWidget {
const LanguageSettings({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = context.locale;
final textController = useTextEditingController(
text: locales.keys.firstWhere(
(countryName) => locales[countryName] == currentLocale,
),
);
final selectedLocale = useState<Locale>(currentLocale);
return ListView(
padding: const EdgeInsets.all(16),
children: [
LayoutBuilder(
builder: (context, constraints) {
return DropdownMenu(
width: constraints.maxWidth,
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.only(left: 16),
),
menuStyle: MenuStyle(
shape: MaterialStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
backgroundColor: MaterialStatePropertyAll<Color>(
context.isDarkTheme
? Colors.grey[900]!
: context.scaffoldBackgroundColor,
),
),
menuHeight: context.height * 0.5,
hintText: "Languages",
label: const Text('Languages'),
dropdownMenuEntries: locales.keys
.map(
(countryName) => DropdownMenuEntry(
value: locales[countryName],
label: countryName,
),
)
.toList(),
controller: textController,
onSelected: (value) {
if (value != null) {
selectedLocale.value = value;
}
},
);
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: selectedLocale.value == currentLocale
? null
: () {
context.setLocale(selectedLocale.value);
loadTranslations();
},
child: const Text('setting_languages_apply').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/entities/duplicated_asset.entity.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/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

@ -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/providers/notification_permission.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/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

@ -0,0 +1,38 @@
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/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
class HapticSetting extends HookConsumerWidget {
const HapticSetting({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hapticFeedbackSetting =
useAppSettingsState(AppSettingsEnum.enableHapticFeedback);
final isHapticFeedbackEnabled =
useValueNotifier(hapticFeedbackSetting.value);
onHapticFeedbackChange(bool isEnabled) {
hapticFeedbackSetting.value = isEnabled;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsSubTitle(title: "haptic_feedback_title".tr()),
SettingsSwitchListTile(
valueNotifier: isHapticFeedbackEnabled,
title: 'haptic_feedback_switch'.tr(),
onChanged: onHapticFeedbackChange,
),
],
);
}
}

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/settings/preference_settings/haptic_setting.dart';
import 'package:immich_mobile/widgets/settings/preference_settings/theme_setting.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
class PreferenceSetting extends StatelessWidget {
const PreferenceSetting({
super.key,
});
@override
Widget build(BuildContext context) {
const preferenceSettings = [
ThemeSetting(),
HapticSetting(),
];
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/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/widgets/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

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class SettingsSwitchListTile extends StatelessWidget {
final ValueNotifier<bool> valueNotifier;
final String title;
final bool enabled;
final String? subtitle;
final IconData? icon;
final Function(bool)? onChanged;
const SettingsSwitchListTile({
required this.valueNotifier,
required this.title,
this.subtitle,
this.icon,
this.enabled = true,
this.onChanged,
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: 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.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
color: enabled ? null : context.themeData.disabledColor,
height: 1.5,
),
),
subtitle: subtitle != null
? Text(
subtitle!,
style: context.textTheme.bodyMedium?.copyWith(
color: enabled ? null : context.themeData.disabledColor,
),
)
: null,
);
}
}

View file

@ -0,0 +1,18 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/entities/store.entity.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;
}