feat: add configurable backup on charging only and delay settings for android (#22114)
* feat: add configurable on charging only and delay * Segmented and style the settings --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -43,6 +43,17 @@ class BackgroundWorkerFgService {
|
||||
// TODO: Move this call to native side once old timeline is removed
|
||||
Future<void> enable() => _foregroundHostApi.enable();
|
||||
|
||||
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure(
|
||||
BackgroundWorkerSettings(
|
||||
minimumDelaySeconds:
|
||||
minimumDelaySeconds ??
|
||||
Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue),
|
||||
requiresCharging:
|
||||
requireCharging ??
|
||||
Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> disable() => _foregroundHostApi.disable();
|
||||
}
|
||||
|
||||
|
||||
+79
@@ -25,6 +25,57 @@ List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length &&
|
||||
a.entries.every(
|
||||
(MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
|
||||
);
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
class BackgroundWorkerSettings {
|
||||
BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds});
|
||||
|
||||
bool requiresCharging;
|
||||
|
||||
int minimumDelaySeconds;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[requiresCharging, minimumDelaySeconds];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static BackgroundWorkerSettings decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return BackgroundWorkerSettings(requiresCharging: result[0]! as bool, minimumDelaySeconds: result[1]! as int);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! BackgroundWorkerSettings || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -32,6 +83,9 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is BackgroundWorkerSettings) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
@@ -40,6 +94,8 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
return BackgroundWorkerSettings.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
@@ -82,6 +138,29 @@ class BackgroundWorkerFgHostApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> configure(BackgroundWorkerSettings settings) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[settings]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disable() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix';
|
||||
|
||||
@@ -50,6 +50,8 @@ enum AppSettingsEnum<T> {
|
||||
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
||||
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
|
||||
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),
|
||||
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
|
||||
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30),
|
||||
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
@@ -18,12 +23,40 @@ class DriftBackupSettings extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const SettingsSubPageScaffold(
|
||||
return SettingsSubPageScaffold(
|
||||
settings: [
|
||||
_UseWifiForUploadVideosButton(),
|
||||
_UseWifiForUploadPhotosButton(),
|
||||
Divider(indent: 16, endIndent: 16),
|
||||
_AlbumSyncActionButton(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
"network_requirements".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
),
|
||||
const _UseWifiForUploadVideosButton(),
|
||||
const _UseWifiForUploadPhotosButton(),
|
||||
if (CurrentPlatform.isAndroid) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
"background_options".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
const _BackupOnlyWhenChargingButton(),
|
||||
const _BackupDelaySlider(),
|
||||
],
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
"backup_albums_sync".t(context: context).toUpperCase(),
|
||||
style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)),
|
||||
),
|
||||
),
|
||||
const _AlbumSyncActionButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -151,30 +184,59 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
|
||||
}
|
||||
}
|
||||
|
||||
class _UseWifiForUploadVideosButton extends ConsumerWidget {
|
||||
const _UseWifiForUploadVideosButton();
|
||||
class _SettingsSwitchTile extends ConsumerStatefulWidget {
|
||||
final AppSettingsEnum<bool> appSettingsEnum;
|
||||
final String titleKey;
|
||||
final String subtitleKey;
|
||||
final void Function(bool?)? onChanged;
|
||||
|
||||
const _SettingsSwitchTile({
|
||||
required this.appSettingsEnum,
|
||||
required this.titleKey,
|
||||
required this.subtitleKey,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final valueStream = Store.watch(StoreKey.useWifiForUploadVideos);
|
||||
ConsumerState createState() => _SettingsSwitchTileState();
|
||||
}
|
||||
|
||||
class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
|
||||
late final Stream<bool?> valueStream;
|
||||
late final StreamSubscription<bool?> subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
valueStream = Store.watch(widget.appSettingsEnum.storeKey).asBroadcastStream();
|
||||
subscription = valueStream.listen((value) {
|
||||
widget.onChanged?.call(value);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"videos".t(context: context),
|
||||
widget.titleKey.t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge),
|
||||
subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge),
|
||||
trailing: StreamBuilder(
|
||||
stream: valueStream,
|
||||
initialData: Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false,
|
||||
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
|
||||
builder: (context, snapshot) {
|
||||
final value = snapshot.data ?? false;
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.setSetting(AppSettingsEnum.useCellularForUploadVideos, newValue);
|
||||
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -183,34 +245,135 @@ class _UseWifiForUploadVideosButton extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _UseWifiForUploadVideosButton extends ConsumerWidget {
|
||||
const _UseWifiForUploadVideosButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const _SettingsSwitchTile(
|
||||
appSettingsEnum: AppSettingsEnum.useCellularForUploadVideos,
|
||||
titleKey: "videos",
|
||||
subtitleKey: "network_requirement_videos_upload",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UseWifiForUploadPhotosButton extends ConsumerWidget {
|
||||
const _UseWifiForUploadPhotosButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final valueStream = Store.watch(StoreKey.useWifiForUploadPhotos);
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"photos".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge),
|
||||
trailing: StreamBuilder(
|
||||
stream: valueStream,
|
||||
initialData: Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false,
|
||||
builder: (context, snapshot) {
|
||||
final value = snapshot.data ?? false;
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (bool newValue) async {
|
||||
await ref
|
||||
.read(appSettingsServiceProvider)
|
||||
.setSetting(AppSettingsEnum.useCellularForUploadPhotos, newValue);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
return const _SettingsSwitchTile(
|
||||
appSettingsEnum: AppSettingsEnum.useCellularForUploadPhotos,
|
||||
titleKey: "photos",
|
||||
subtitleKey: "network_requirement_photos_upload",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackupOnlyWhenChargingButton extends ConsumerWidget {
|
||||
const _BackupOnlyWhenChargingButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _SettingsSwitchTile(
|
||||
appSettingsEnum: AppSettingsEnum.backupRequireCharging,
|
||||
titleKey: "charging",
|
||||
subtitleKey: "charging_requirement_mobile_backup",
|
||||
onChanged: (value) {
|
||||
ref.read(backgroundWorkerFgServiceProvider).configure(requireCharging: value ?? false);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackupDelaySlider extends ConsumerStatefulWidget {
|
||||
const _BackupDelaySlider();
|
||||
|
||||
@override
|
||||
ConsumerState<_BackupDelaySlider> createState() => _BackupDelaySliderState();
|
||||
}
|
||||
|
||||
class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
|
||||
late final Stream<int?> valueStream;
|
||||
late final StreamSubscription<int?> subscription;
|
||||
late int currentValue;
|
||||
|
||||
static int backupDelayToSliderValue(int ms) => switch (ms) {
|
||||
5 => 0,
|
||||
30 => 1,
|
||||
120 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
|
||||
static int backupDelayToSeconds(int v) => switch (v) {
|
||||
0 => 5,
|
||||
1 => 30,
|
||||
2 => 120,
|
||||
_ => 600,
|
||||
};
|
||||
|
||||
static String formatBackupDelaySliderValue(int v) => switch (v) {
|
||||
0 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '5'}),
|
||||
1 => 'setting_notifications_notify_seconds'.tr(namedArgs: {'count': '30'}),
|
||||
2 => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '2'}),
|
||||
_ => 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '10'}),
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialValue =
|
||||
Store.tryGet(AppSettingsEnum.backupTriggerDelay.storeKey) ?? AppSettingsEnum.backupTriggerDelay.defaultValue;
|
||||
currentValue = backupDelayToSliderValue(initialValue);
|
||||
|
||||
valueStream = Store.watch(AppSettingsEnum.backupTriggerDelay.storeKey).asBroadcastStream();
|
||||
subscription = valueStream.listen((value) {
|
||||
if (mounted && value != null) {
|
||||
setState(() {
|
||||
currentValue = backupDelayToSliderValue(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 8.0),
|
||||
child: Text(
|
||||
'backup_controller_page_background_delay'.tr(
|
||||
namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)},
|
||||
),
|
||||
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: currentValue.toDouble(),
|
||||
onChanged: (double v) {
|
||||
setState(() {
|
||||
currentValue = v.toInt();
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double v) async {
|
||||
final milliseconds = backupDelayToSeconds(v.toInt());
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.backupTriggerDelay, milliseconds);
|
||||
},
|
||||
max: 3.0,
|
||||
min: 0.0,
|
||||
divisions: 3,
|
||||
label: formatBackupDelaySliderValue(currentValue),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user