mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): iOS background sync (#1758)
* first run of getting background sync working in iOS * got background sync calling into flutter * added background task * added necessary sync files * fixed some names and added more implementations * got as far as Hive.initFlutter * brute force got to await Hive.initFlutter * lots of print statements to figure out where execution is failing, and its failing at the root asset bundle in the localization.dart service * first time working, got plugins registered * removed broken cleanup code * refactored * linters * now can pass user settings * background service plugin uses app background processing instead of fetch * renamed backgroundFetch to backgroundProcessing to make it clearer * don't use max delay * adds fetch back in * fixes require charging default values and backup controller page * fixes background fetch * fixes ios not importing photos * guarded path provider ios * lint * adds max tries for heartbeat to work in iOS * fail after seconds * timeout instead of fail after seconds * removes release lock from system stop * restores checkLockReleasedWithHeartbeat to Future<void> * removes max tries from acquire lock * fixes lock timeout with iOS * restored for loop * adds comments, made the AppRefresh task only run while not requiring network or charge * fixed compile issue * now both are registered and added better comments. also added ability for task to cancel itself * added the podfile and pubspec * added backup diagnostics to IOS and removed iOS ignored backup options and fixed network connectivity always required * Added Alex's dev team * styled debug list item, fixed refresh task not set bug, fixed enable / disable background service on platform channel --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui' show IsolateNameServer, PluginUtilities;
|
||||
import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||
@@ -19,6 +18,7 @@ import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
final backgroundServiceProvider = Provider(
|
||||
@@ -51,8 +51,7 @@ class BackgroundService {
|
||||
late final _Throttle _throttledDetailNotify =
|
||||
_Throttle(_updateDetailProgress, notifyInterval);
|
||||
Completer<bool> _hasAccessCompleter = Completer();
|
||||
late Future<bool> _hasAccess =
|
||||
Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
|
||||
late Future<bool> _hasAccess = _hasAccessCompleter.future;
|
||||
|
||||
Future<bool> get hasAccess => _hasAccess;
|
||||
|
||||
@@ -67,9 +66,6 @@ class BackgroundService {
|
||||
|
||||
/// Enqueues the background service
|
||||
Future<bool> enableService({bool immediate = false}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
||||
final String title =
|
||||
@@ -89,9 +85,6 @@ class BackgroundService {
|
||||
int triggerUpdateDelay = 5000,
|
||||
int triggerMaxDelay = 50000,
|
||||
}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
final bool ok = await _foregroundChannel.invokeMethod(
|
||||
'configure',
|
||||
@@ -110,9 +103,6 @@ class BackgroundService {
|
||||
|
||||
/// Cancels the background service (if currently running) and removes it from work queue
|
||||
Future<bool> disableService() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
final ok = await _foregroundChannel.invokeMethod('disable');
|
||||
return ok;
|
||||
@@ -123,9 +113,6 @@ class BackgroundService {
|
||||
|
||||
/// Returns `true` if the background service is enabled
|
||||
Future<bool> isBackgroundBackupEnabled() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await _foregroundChannel.invokeMethod("isEnabled");
|
||||
} catch (error) {
|
||||
@@ -135,7 +122,8 @@ class BackgroundService {
|
||||
|
||||
/// Returns `true` if battery optimizations are disabled
|
||||
Future<bool> isIgnoringBatteryOptimizations() async {
|
||||
if (!Platform.isAndroid) {
|
||||
// iOS does not need battery optimizations enabled
|
||||
if (Platform.isIOS) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
@@ -156,9 +144,6 @@ class BackgroundService {
|
||||
bool isDetail = false,
|
||||
bool onlyIfFG = false,
|
||||
}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (_isBackgroundInitialized) {
|
||||
return _backgroundChannel.invokeMethod<bool>(
|
||||
@@ -178,9 +163,6 @@ class BackgroundService {
|
||||
String? content,
|
||||
String? individualTag,
|
||||
}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
|
||||
return await _backgroundChannel
|
||||
@@ -193,9 +175,6 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<bool> _clearErrorNotifications() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (_isBackgroundInitialized) {
|
||||
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
|
||||
@@ -210,9 +189,6 @@ class BackgroundService {
|
||||
|
||||
/// await to ensure this thread (foreground or background) has exclusive access
|
||||
Future<bool> acquireLock() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return true;
|
||||
}
|
||||
if (_hasLock) {
|
||||
debugPrint("WARNING: [acquireLock] called more than once");
|
||||
return true;
|
||||
@@ -253,7 +229,7 @@ class BackgroundService {
|
||||
while (_wantsLockTime == lockTime) {
|
||||
other.send(tempSp);
|
||||
final dynamic answer = await bs.first
|
||||
.timeout(const Duration(seconds: 5), onTimeout: () => null);
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => null);
|
||||
if (_wantsLockTime != lockTime) {
|
||||
break;
|
||||
}
|
||||
@@ -270,7 +246,7 @@ class BackgroundService {
|
||||
// other isolate is still active
|
||||
}
|
||||
final dynamic isFinished = await bs.first
|
||||
.timeout(const Duration(seconds: 5), onTimeout: () => false);
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => false);
|
||||
if (isFinished == true) {
|
||||
break;
|
||||
}
|
||||
@@ -288,9 +264,6 @@ class BackgroundService {
|
||||
|
||||
/// releases the exclusive access lock
|
||||
void releaseLock() {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
_wantsLockTime = 0;
|
||||
if (_hasLock) {
|
||||
_hasAccessCompleter = Completer();
|
||||
@@ -311,17 +284,35 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<bool> _callHandler(MethodCall call) async {
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
if (Platform.isIOS) {
|
||||
// NOTE: I'm not sure this is strictly necessary anymore, but
|
||||
// out of an abundance of caution, we will keep it in until someone
|
||||
// can say for sure
|
||||
PathProviderIOS.registerWith();
|
||||
}
|
||||
switch (call.method) {
|
||||
case "backgroundProcessing":
|
||||
case "onAssetsChanged":
|
||||
final Future<bool> translationsLoaded = loadTranslations();
|
||||
try {
|
||||
_clearErrorNotifications();
|
||||
final bool hasAccess = await acquireLock();
|
||||
|
||||
// iOS should time out after some threshhold so it doesn't wait
|
||||
// indefinitely and can run later
|
||||
// Android is fine to wait here until the lock releases
|
||||
final waitForLock = Platform.isIOS
|
||||
? acquireLock()
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => false,
|
||||
)
|
||||
: acquireLock();
|
||||
|
||||
final bool hasAccess = await waitForLock;
|
||||
if (!hasAccess) {
|
||||
debugPrint("[_callHandler] could not acquire lock, exiting");
|
||||
return false;
|
||||
}
|
||||
await translationsLoaded;
|
||||
final bool ok = await _onAssetsChanged();
|
||||
return ok;
|
||||
} catch (error) {
|
||||
@@ -388,9 +379,9 @@ class BackgroundService {
|
||||
.put(backupFailedSince, DateTime.now());
|
||||
return false;
|
||||
}
|
||||
// check for new assets added while performing backup
|
||||
} while (true ==
|
||||
await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
|
||||
// Android should check for new assets added while performing backup
|
||||
} while (Platform.isAndroid &&
|
||||
true == await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -560,6 +551,28 @@ class BackgroundService {
|
||||
final String total = numberFormat.format(bytesTotal / 1024.0);
|
||||
return "$percent% ($done/$total$unit)";
|
||||
}
|
||||
|
||||
Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async {
|
||||
// Seconds since last run
|
||||
final double? lastRun = task == IosBackgroundTask.fetch
|
||||
? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime')
|
||||
: await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime');
|
||||
if (lastRun == null) {
|
||||
return null;
|
||||
}
|
||||
final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000);
|
||||
return time;
|
||||
}
|
||||
|
||||
Future<int> getIOSBackupNumberOfProcesses() async {
|
||||
return await _foregroundChannel
|
||||
.invokeMethod('numberOfBackgroundProcesses');
|
||||
}
|
||||
}
|
||||
|
||||
enum IosBackgroundTask {
|
||||
fetch,
|
||||
processing
|
||||
}
|
||||
|
||||
class _Throttle {
|
||||
@@ -603,6 +616,7 @@ class _Throttle {
|
||||
@pragma('vm:entry-point')
|
||||
void _nativeEntry() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
BackgroundService backgroundService = BackgroundService();
|
||||
backgroundService._setupBackgroundCallHandler();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
@@ -130,7 +128,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
requireCharging != null ||
|
||||
triggerDelay != null,
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
final bool wasEnabled = state.backgroundBackup;
|
||||
final bool wasWifi = state.backupRequireWifi;
|
||||
final bool wasCharging = state.backupRequireCharging;
|
||||
@@ -180,7 +177,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
onError("backup_controller_page_background_configure_error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
@@ -577,48 +573,47 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
}
|
||||
|
||||
Future<void> resumeBackup() async {
|
||||
if (Platform.isAndroid) {
|
||||
// assumes the background service is currently running
|
||||
// if true, waits until it has stopped to update the app state from HiveDB
|
||||
// before actually resuming backup by calling the internal `_resumeBackup`
|
||||
final BackUpProgressEnum previous = state.backupProgress;
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||
final bool hasLock = await _backgroundService.acquireLock();
|
||||
if (!hasLock) {
|
||||
log.warning("WARNING [resumeBackup] failed to acquireLock");
|
||||
return;
|
||||
}
|
||||
await Future.wait([
|
||||
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||
Hive.openBox(backgroundBackupInfoBox),
|
||||
]);
|
||||
final HiveBackupAlbums? albums =
|
||||
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
|
||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||
if (albums != null) {
|
||||
selectedAlbums = _updateAlbumsBackupTime(
|
||||
selectedAlbums,
|
||||
albums.selectedAlbumIds,
|
||||
albums.lastSelectedBackupTime,
|
||||
);
|
||||
excludedAlbums = _updateAlbumsBackupTime(
|
||||
excludedAlbums,
|
||||
albums.excludedAlbumsIds,
|
||||
albums.lastExcludedBackupTime,
|
||||
);
|
||||
}
|
||||
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
|
||||
state = state.copyWith(
|
||||
backupProgress: previous,
|
||||
selectedBackupAlbums: selectedAlbums,
|
||||
excludedBackupAlbums: excludedAlbums,
|
||||
backupRequireWifi: backgroundBox.get(backupRequireWifi),
|
||||
backupRequireCharging: backgroundBox.get(backupRequireCharging),
|
||||
backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
|
||||
|
||||
// assumes the background service is currently running
|
||||
// if true, waits until it has stopped to update the app state from HiveDB
|
||||
// before actually resuming backup by calling the internal `_resumeBackup`
|
||||
final BackUpProgressEnum previous = state.backupProgress;
|
||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||
final bool hasLock = await _backgroundService.acquireLock();
|
||||
if (!hasLock) {
|
||||
log.warning("WARNING [resumeBackup] failed to acquireLock");
|
||||
return;
|
||||
}
|
||||
await Future.wait([
|
||||
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||
Hive.openBox(backgroundBackupInfoBox),
|
||||
]);
|
||||
final HiveBackupAlbums? albums =
|
||||
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
|
||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||
if (albums != null) {
|
||||
selectedAlbums = _updateAlbumsBackupTime(
|
||||
selectedAlbums,
|
||||
albums.selectedAlbumIds,
|
||||
albums.lastSelectedBackupTime,
|
||||
);
|
||||
excludedAlbums = _updateAlbumsBackupTime(
|
||||
excludedAlbums,
|
||||
albums.excludedAlbumsIds,
|
||||
albums.lastExcludedBackupTime,
|
||||
);
|
||||
}
|
||||
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
|
||||
state = state.copyWith(
|
||||
backupProgress: previous,
|
||||
selectedBackupAlbums: selectedAlbums,
|
||||
excludedBackupAlbums: excludedAlbums,
|
||||
backupRequireWifi: backgroundBox.get(backupRequireWifi),
|
||||
backupRequireCharging: backgroundBox.get(backupRequireCharging),
|
||||
backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
|
||||
);
|
||||
return _resumeBackup();
|
||||
}
|
||||
|
||||
@@ -649,8 +644,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
AppStateEnum.paused,
|
||||
AppStateEnum.detached,
|
||||
];
|
||||
if (Platform.isAndroid &&
|
||||
allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
|
||||
if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
|
||||
try {
|
||||
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
|
||||
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
|
||||
@@ -683,6 +677,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
_backgroundService.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final backupProvider =
|
||||
|
||||
67
mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
Normal file
67
mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// This is a simple debug widget which should be removed later on when we are
|
||||
/// more confident about background sync
|
||||
class IosDebugInfoTile extends HookConsumerWidget {
|
||||
const IosDebugInfoTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final futures = [
|
||||
ref.read(backgroundServiceProvider)
|
||||
.getIOSBackupLastRun(IosBackgroundTask.fetch),
|
||||
ref.read(backgroundServiceProvider)
|
||||
.getIOSBackupLastRun(IosBackgroundTask.processing),
|
||||
ref.read(backgroundServiceProvider)
|
||||
.getIOSBackupNumberOfProcesses(),
|
||||
];
|
||||
return FutureBuilder<List<dynamic>>(
|
||||
future: Future.wait(futures),
|
||||
builder: (context, snapshot) {
|
||||
String? title;
|
||||
String? subtitle;
|
||||
if (snapshot.hasData) {
|
||||
final results = snapshot.data as List<dynamic>;
|
||||
final fetch = results[0] as DateTime?;
|
||||
final processing = results[1] as DateTime?;
|
||||
final processes = results[2] as int;
|
||||
|
||||
final processOrProcesses = processes == 1 ? 'process' : 'processes';
|
||||
final numberOrZero = processes == 0 ? 'No' : processes.toString();
|
||||
title = '$numberOrZero background $processOrProcesses queued';
|
||||
|
||||
final df = DateFormat.yMd().add_jm();
|
||||
if (fetch == null && processing == null) {
|
||||
subtitle = 'No background sync job has run yet';
|
||||
} else if (fetch != null && processing == null) {
|
||||
subtitle = 'Fetch ran ${df.format(fetch)}';
|
||||
} else if (processing != null && fetch == null) {
|
||||
subtitle = 'Processing ran ${df.format(processing)}';
|
||||
} else {
|
||||
final fetchOrProcessing = fetch!.isAfter(processing!)
|
||||
? fetch
|
||||
: processing;
|
||||
subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: ListTile(
|
||||
key: ValueKey(title),
|
||||
title: Text(title ?? ''),
|
||||
subtitle: Text(subtitle ?? ''),
|
||||
leading: Icon(
|
||||
Icons.bug_report,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
|
||||
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
@@ -195,7 +196,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
ListTile buildBackgroundBackupController() {
|
||||
Widget buildBackgroundBackupController() {
|
||||
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
||||
final bool isWifiRequired = backupState.backupRequireWifi;
|
||||
final bool isChargingRequired = backupState.backupRequireCharging;
|
||||
@@ -240,118 +241,126 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
final triggerDelay =
|
||||
useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: isBackgroundEnabled
|
||||
? Icon(
|
||||
Icons.cloud_sync_rounded,
|
||||
color: activeColor,
|
||||
)
|
||||
: const Icon(Icons.cloud_sync_rounded),
|
||||
title: Text(
|
||||
isBackgroundEnabled
|
||||
? "backup_controller_page_background_is_on"
|
||||
: "backup_controller_page_background_is_off",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
).tr(),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isBackgroundEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child:
|
||||
const Text("backup_controller_page_background_description")
|
||||
.tr(),
|
||||
),
|
||||
if (isBackgroundEnabled)
|
||||
SwitchListTile(
|
||||
title:
|
||||
const Text("backup_controller_page_background_wifi").tr(),
|
||||
secondary: Icon(
|
||||
Icons.wifi,
|
||||
color: isWifiRequired ? activeColor : null,
|
||||
),
|
||||
dense: true,
|
||||
activeColor: activeColor,
|
||||
value: isWifiRequired,
|
||||
onChanged: hasExclusiveAccess
|
||||
? (isChecked) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
requireWifi: isChecked,
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (isBackgroundEnabled)
|
||||
SwitchListTile(
|
||||
title: const Text("backup_controller_page_background_charging")
|
||||
.tr(),
|
||||
secondary: Icon(
|
||||
Icons.charging_station,
|
||||
color: isChargingRequired ? activeColor : null,
|
||||
),
|
||||
dense: true,
|
||||
activeColor: activeColor,
|
||||
value: isChargingRequired,
|
||||
onChanged: hasExclusiveAccess
|
||||
? (isChecked) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
requireCharging: isChecked,
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (isBackgroundEnabled)
|
||||
ListTile(
|
||||
isThreeLine: false,
|
||||
dense: true,
|
||||
enabled: hasExclusiveAccess,
|
||||
title: const Text(
|
||||
'backup_controller_page_background_delay',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
leading: isBackgroundEnabled
|
||||
? Icon(
|
||||
Icons.cloud_sync_rounded,
|
||||
color: activeColor,
|
||||
)
|
||||
: const Icon(Icons.cloud_sync_rounded),
|
||||
title: Text(
|
||||
isBackgroundEnabled
|
||||
? "backup_controller_page_background_is_on"
|
||||
: "backup_controller_page_background_is_off",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
).tr(),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isBackgroundEnabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child:
|
||||
const Text("backup_controller_page_background_description")
|
||||
.tr(),
|
||||
),
|
||||
).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
|
||||
subtitle: Slider(
|
||||
value: triggerDelay.value,
|
||||
onChanged: hasExclusiveAccess
|
||||
? (double v) => triggerDelay.value = v
|
||||
: null,
|
||||
onChangeEnd: (double v) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
triggerDelay: backupDelayToMilliseconds(v),
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
if (isBackgroundEnabled && Platform.isAndroid)
|
||||
SwitchListTile(
|
||||
title:
|
||||
const Text("backup_controller_page_background_wifi").tr(),
|
||||
secondary: Icon(
|
||||
Icons.wifi,
|
||||
color: isWifiRequired ? activeColor : null,
|
||||
),
|
||||
dense: true,
|
||||
activeColor: activeColor,
|
||||
value: isWifiRequired,
|
||||
onChanged: hasExclusiveAccess
|
||||
? (isChecked) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
requireWifi: isChecked,
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (isBackgroundEnabled)
|
||||
SwitchListTile(
|
||||
title: const Text("backup_controller_page_background_charging")
|
||||
.tr(),
|
||||
secondary: Icon(
|
||||
Icons.charging_station,
|
||||
color: isChargingRequired ? activeColor : null,
|
||||
),
|
||||
dense: true,
|
||||
activeColor: activeColor,
|
||||
value: isChargingRequired,
|
||||
onChanged: hasExclusiveAccess
|
||||
? (isChecked) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
requireCharging: isChecked,
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (isBackgroundEnabled && Platform.isAndroid)
|
||||
ListTile(
|
||||
isThreeLine: false,
|
||||
dense: true,
|
||||
enabled: hasExclusiveAccess,
|
||||
title: const Text(
|
||||
'backup_controller_page_background_delay',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
max: 3.0,
|
||||
divisions: 3,
|
||||
label: formatBackupDelaySliderValue(triggerDelay.value),
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
|
||||
subtitle: Slider(
|
||||
value: triggerDelay.value,
|
||||
onChanged: hasExclusiveAccess
|
||||
? (double v) => triggerDelay.value = v
|
||||
: null,
|
||||
onChangeEnd: (double v) => ref
|
||||
.read(backupProvider.notifier)
|
||||
.configureBackgroundBackup(
|
||||
triggerDelay: backupDelayToMilliseconds(v),
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
),
|
||||
max: 3.0,
|
||||
divisions: 3,
|
||||
label: formatBackupDelaySliderValue(triggerDelay.value),
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(backupProvider.notifier).configureBackgroundBackup(
|
||||
enabled: !isBackgroundEnabled,
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
),
|
||||
child: Text(
|
||||
isBackgroundEnabled
|
||||
? "backup_controller_page_background_turn_off"
|
||||
: "backup_controller_page_background_turn_on",
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
).tr(),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(backupProvider.notifier).configureBackgroundBackup(
|
||||
enabled: !isBackgroundEnabled,
|
||||
onError: showErrorToUser,
|
||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||
),
|
||||
child: Text(
|
||||
isBackgroundEnabled
|
||||
? "backup_controller_page_background_turn_off"
|
||||
: "backup_controller_page_background_turn_on",
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isBackgroundEnabled)
|
||||
IosDebugInfoTile(
|
||||
key: ValueKey(isChargingRequired),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -592,8 +601,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
),
|
||||
const Divider(),
|
||||
buildAutoBackupController(),
|
||||
if (Platform.isAndroid) const Divider(),
|
||||
if (Platform.isAndroid) buildBackgroundBackupController(),
|
||||
const Divider(),
|
||||
buildBackgroundBackupController(),
|
||||
const Divider(),
|
||||
buildStorageInformation(),
|
||||
const Divider(),
|
||||
|
||||
Reference in New Issue
Block a user