mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
parent
7520ffd6c3
commit
5806a3ce25
203 changed files with 318 additions and 318 deletions
69
mobile/lib/widgets/settings/advanced_settings.dart
Normal file
69
mobile/lib/widgets/settings/advanced_settings.dart
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import 'dart:io';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:immich_mobile/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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
81
mobile/lib/widgets/settings/language_settings.dart
Normal file
81
mobile/lib/widgets/settings/language_settings.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
54
mobile/lib/widgets/settings/local_storage_settings.dart
Normal file
54
mobile/lib/widgets/settings/local_storage_settings.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState;
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
mobile/lib/widgets/settings/notification_setting.dart
Normal file
118
mobile/lib/widgets/settings/notification_setting.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
50
mobile/lib/widgets/settings/settings_button_list_tile.dart
Normal file
50
mobile/lib/widgets/settings/settings_button_list_tile.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
mobile/lib/widgets/settings/settings_radio_list_tile.dart
Normal file
47
mobile/lib/widgets/settings/settings_radio_list_tile.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class SettingsRadioGroup<T> {
|
||||
final String title;
|
||||
final T value;
|
||||
|
||||
SettingsRadioGroup({required this.title, required this.value});
|
||||
}
|
||||
|
||||
class SettingsRadioListTile<T> extends StatelessWidget {
|
||||
final List<SettingsRadioGroup> groups;
|
||||
final T groupBy;
|
||||
final void Function(T?) onRadioChanged;
|
||||
|
||||
const SettingsRadioListTile({
|
||||
super.key,
|
||||
required this.groups,
|
||||
required this.groupBy,
|
||||
required this.onRadioChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: groups
|
||||
.map(
|
||||
(g) => RadioListTile<T>(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
dense: true,
|
||||
activeColor: context.primaryColor,
|
||||
title: Text(
|
||||
g.title,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
value: g.value,
|
||||
groupValue: groupBy,
|
||||
onChanged: onRadioChanged,
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
mobile/lib/widgets/settings/settings_slider_list_tile.dart
Normal file
49
mobile/lib/widgets/settings/settings_slider_list_tile.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
mobile/lib/widgets/settings/settings_sub_page_scaffold.dart
Normal file
30
mobile/lib/widgets/settings/settings_sub_page_scaffold.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
mobile/lib/widgets/settings/settings_sub_title.dart
Normal file
25
mobile/lib/widgets/settings/settings_sub_title.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class SettingsSubTitle extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const SettingsSubTitle({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Text(
|
||||
title,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
mobile/lib/widgets/settings/settings_switch_list_tile.dart
Normal file
63
mobile/lib/widgets/settings/settings_switch_list_tile.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue