diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 75168ce1c..c6c23d942 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -169,4 +169,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index 1deddddcd..ad795a6f5 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -342,7 +342,8 @@ class BackgroundService { ApiService apiService = ApiService(); apiService.setAccessToken(Store.get(StoreKey.accessToken)); - BackupService backupService = BackupService(apiService, db); + AppSettingsService settingService = AppSettingsService(); + BackupService backupService = BackupService(apiService, db, settingService); AppSettingsService settingsService = AppSettingsService(); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); @@ -452,9 +453,12 @@ class BackgroundService { ); _cancellationToken = CancellationToken(); + final pmProgressHandler = PMProgressHandler(); + final bool ok = await backupService.backupAsset( toUpload, _cancellationToken!, + pmProgressHandler, notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, notifySingleProgress ? _onProgress : (sent, total) {}, notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, diff --git a/mobile/lib/modules/backup/models/backup_state.model.dart b/mobile/lib/modules/backup/models/backup_state.model.dart index e557e05be..dd90251b8 100644 --- a/mobile/lib/modules/backup/models/backup_state.model.dart +++ b/mobile/lib/modules/backup/models/backup_state.model.dart @@ -1,10 +1,12 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first + import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; -import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; +import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart'; enum BackUpProgressEnum { idle, @@ -19,6 +21,7 @@ class BackUpState { final BackUpProgressEnum backupProgress; final List allAssetsInDatabase; final double progressInPercentage; + final double iCloudDownloadProgress; final CancellationToken cancelToken; final ServerDiskInfo serverInfo; final bool autoBackup; @@ -45,6 +48,7 @@ class BackUpState { required this.backupProgress, required this.allAssetsInDatabase, required this.progressInPercentage, + required this.iCloudDownloadProgress, required this.cancelToken, required this.serverInfo, required this.autoBackup, @@ -64,6 +68,7 @@ class BackUpState { BackUpProgressEnum? backupProgress, List? allAssetsInDatabase, double? progressInPercentage, + double? iCloudDownloadProgress, CancellationToken? cancelToken, ServerDiskInfo? serverInfo, bool? autoBackup, @@ -82,6 +87,8 @@ class BackUpState { backupProgress: backupProgress ?? this.backupProgress, allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, progressInPercentage: progressInPercentage ?? this.progressInPercentage, + iCloudDownloadProgress: + iCloudDownloadProgress ?? this.iCloudDownloadProgress, cancelToken: cancelToken ?? this.cancelToken, serverInfo: serverInfo ?? this.serverInfo, autoBackup: autoBackup ?? this.autoBackup, @@ -102,18 +109,18 @@ class BackUpState { @override String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; + return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; } @override - bool operator ==(Object other) { + bool operator ==(covariant BackUpState other) { if (identical(this, other)) return true; final collectionEquals = const DeepCollectionEquality().equals; - return other is BackUpState && - other.backupProgress == backupProgress && + return other.backupProgress == backupProgress && collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && other.progressInPercentage == progressInPercentage && + other.iCloudDownloadProgress == iCloudDownloadProgress && other.cancelToken == cancelToken && other.serverInfo == serverInfo && other.autoBackup == autoBackup && @@ -137,6 +144,7 @@ class BackUpState { return backupProgress.hashCode ^ allAssetsInDatabase.hashCode ^ progressInPercentage.hashCode ^ + iCloudDownloadProgress.hashCode ^ cancelToken.hashCode ^ serverInfo.hashCode ^ autoBackup.hashCode ^ diff --git a/mobile/lib/modules/backup/models/current_upload_asset.model.dart b/mobile/lib/modules/backup/models/current_upload_asset.model.dart index ebcd99a20..ae75f68f8 100644 --- a/mobile/lib/modules/backup/models/current_upload_asset.model.dart +++ b/mobile/lib/modules/backup/models/current_upload_asset.model.dart @@ -1,3 +1,4 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; class CurrentUploadAsset { @@ -5,12 +6,14 @@ class CurrentUploadAsset { final DateTime fileCreatedAt; final String fileName; final String fileType; + final bool? iCloudAsset; CurrentUploadAsset({ required this.id, required this.fileCreatedAt, required this.fileName, required this.fileType, + this.iCloudAsset, }); CurrentUploadAsset copyWith({ @@ -18,54 +21,58 @@ class CurrentUploadAsset { DateTime? fileCreatedAt, String? fileName, String? fileType, + bool? iCloudAsset, }) { return CurrentUploadAsset( id: id ?? this.id, fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, fileName: fileName ?? this.fileName, fileType: fileType ?? this.fileType, + iCloudAsset: iCloudAsset ?? this.iCloudAsset, ); } Map toMap() { - final result = {}; - - result.addAll({'id': id}); - result.addAll({'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch}); - result.addAll({'fileName': fileName}); - result.addAll({'fileType': fileType}); - - return result; + return { + 'id': id, + 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, + 'fileName': fileName, + 'fileType': fileType, + 'iCloudAsset': iCloudAsset, + }; } factory CurrentUploadAsset.fromMap(Map map) { return CurrentUploadAsset( - id: map['id'] ?? '', - fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt']), - fileName: map['fileName'] ?? '', - fileType: map['fileType'] ?? '', + id: map['id'] as String, + fileCreatedAt: + DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), + fileName: map['fileName'] as String, + fileType: map['fileType'] as String, + iCloudAsset: + map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, ); } String toJson() => json.encode(toMap()); factory CurrentUploadAsset.fromJson(String source) => - CurrentUploadAsset.fromMap(json.decode(source)); + CurrentUploadAsset.fromMap(json.decode(source) as Map); @override String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType)'; + return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, iCloudAsset: $iCloudAsset)'; } @override - bool operator ==(Object other) { + bool operator ==(covariant CurrentUploadAsset other) { if (identical(this, other)) return true; - return other is CurrentUploadAsset && - other.id == id && + return other.id == id && other.fileCreatedAt == fileCreatedAt && other.fileName == fileName && - other.fileType == fileType; + other.fileType == fileType && + other.iCloudAsset == iCloudAsset; } @override @@ -73,6 +80,7 @@ class CurrentUploadAsset { return id.hashCode ^ fileCreatedAt.hashCode ^ fileName.hashCode ^ - fileType.hashCode; + fileType.hashCode ^ + iCloudAsset.hashCode; } } diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index 8d50eab6e..366318489 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -61,7 +61,9 @@ class BackupNotifier extends StateNotifier { fileCreatedAt: DateTime.parse('2020-10-04'), fileName: '...', fileType: '...', + iCloudAsset: false, ), + iCloudDownloadProgress: 0.0, ), ); @@ -444,9 +446,18 @@ class BackupNotifier extends StateNotifier { // Perform Backup state = state.copyWith(cancelToken: CancellationToken()); + + final pmProgressHandler = PMProgressHandler(); + + pmProgressHandler.stream.listen((event) { + final double progress = event.progress; + state = state.copyWith(iCloudDownloadProgress: progress); + }); + await _backupService.backupAsset( assetsWillBeBackup, state.cancelToken, + pmProgressHandler, _onAssetUploaded, _onUploadProgress, _onSetCurrentBackupAsset, diff --git a/mobile/lib/modules/backup/providers/manual_upload.provider.dart b/mobile/lib/modules/backup/providers/manual_upload.provider.dart index 4d643b9a1..449fd390d 100644 --- a/mobile/lib/modules/backup/providers/manual_upload.provider.dart +++ b/mobile/lib/modules/backup/providers/manual_upload.provider.dart @@ -208,10 +208,12 @@ class ManualUploadNotifier extends StateNotifier { state.totalAssetsToUpload == 1; state = state.copyWith(showDetailedNotification: showDetailedNotification); + final pmProgressHandler = PMProgressHandler(); final bool ok = await ref.read(backupServiceProvider).backupAsset( allUploadAssets, state.cancelToken, + pmProgressHandler, _onAssetUploaded, _onProgress, _onSetCurrentBackupAsset, diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index f4ca5932a..32d2eaa4a 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -10,6 +10,8 @@ import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; @@ -26,6 +28,7 @@ final backupServiceProvider = Provider( (ref) => BackupService( ref.watch(apiServiceProvider), ref.watch(dbProvider), + ref.watch(appSettingsServiceProvider), ), ); @@ -34,8 +37,9 @@ class BackupService { final ApiService _apiService; final Isar _db; final Logger _log = Logger("BackupService"); + final AppSettingsService _appSetting; - BackupService(this._apiService, this._db); + BackupService(this._apiService, this._db, this._appSetting); Future?> getDeviceBackupAsset() async { final String deviceId = Store.get(StoreKey.deviceId); @@ -202,12 +206,16 @@ class BackupService { Future backupAsset( Iterable assetList, http.CancellationToken cancelToken, + PMProgressHandler pmProgressHandler, Function(String, String, bool) uploadSuccessCb, Function(int, int) uploadProgressCb, Function(CurrentUploadAsset) setCurrentUploadAssetCb, Function(ErrorUploadAsset) errorCb, { bool sortAssets = false, }) async { + final bool isIgnoreIcloudAssets = + _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); + if (Platform.isAndroid && !(await Permission.accessMediaLocation.status).isGranted) { // double check that permission is granted here, to guard against @@ -241,10 +249,34 @@ class BackupService { for (var entity in assetsToUpload) { try { - if (entity.type == AssetType.video) { - file = await entity.originFile; + final isAvailableLocally = await entity.isLocallyAvailable(); + + // Handle getting files from iCloud + if (!isAvailableLocally && Platform.isIOS) { + // Skip iCloud assets if the user has disabled this feature + if (isIgnoreIcloudAssets) { + continue; + } + + setCurrentUploadAssetCb( + CurrentUploadAsset( + id: entity.id, + fileCreatedAt: entity.createDateTime.year == 1970 + ? entity.modifiedDateTime + : entity.createDateTime, + fileName: await entity.titleAsync, + fileType: _getAssetType(entity.type), + iCloudAsset: true, + ), + ); + + file = await entity.loadFile(progressHandler: pmProgressHandler); } else { - file = await entity.originFile.timeout(const Duration(seconds: 5)); + if (entity.type == AssetType.video) { + file = await entity.originFile; + } else { + file = await entity.originFile.timeout(const Duration(seconds: 5)); + } } if (file != null) { @@ -286,6 +318,7 @@ class BackupService { : entity.createDateTime, fileName: originalFileName, fileType: _getAssetType(entity.type), + iCloudAsset: false, ), ); diff --git a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart index 926ccd25b..f4d9e531d 100644 --- a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart +++ b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -23,6 +25,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { var uploadProgress = !isManualUpload ? ref.watch(backupProvider).progressInPercentage : ref.watch(manualUploadProvider).progressInPercentage; + var iCloudDownloadProgress = + ref.watch(backupProvider).iCloudDownloadProgress; final isShowThumbnail = useState(false); String getAssetCreationDate() { @@ -143,6 +147,69 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { } } + buildiCloudDownloadProgerssBar() { + if (asset.iCloudAsset != null && asset.iCloudAsset!) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + children: [ + SizedBox( + width: 110, + child: Text( + "iCloud Download", + style: context.textTheme.labelSmall, + ), + ), + Expanded( + child: LinearProgressIndicator( + minHeight: 10.0, + value: uploadProgress / 100.0, + backgroundColor: Colors.grey, + color: context.primaryColor, + ), + ), + Text( + " ${iCloudDownloadProgress.toStringAsFixed(0)}%", + style: const TextStyle(fontSize: 12), + ), + ], + ), + ); + } + + return const SizedBox(); + } + + buildUploadProgressBar() { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + children: [ + if (asset.iCloudAsset != null && asset.iCloudAsset!) + SizedBox( + width: 110, + child: Text( + "Immich Upload", + style: context.textTheme.labelSmall, + ), + ), + Expanded( + child: LinearProgressIndicator( + minHeight: 10.0, + value: uploadProgress / 100.0, + backgroundColor: Colors.grey, + color: context.primaryColor, + ), + ), + Text( + " ${uploadProgress.toStringAsFixed(0)}%", + style: const TextStyle(fontSize: 12), + ), + ], + ), + ); + } + return FutureBuilder( future: buildAssetThumbnail(), builder: (context, thumbnail) => ListTile( @@ -197,25 +264,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ), subtitle: Column( children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Row( - children: [ - Expanded( - child: LinearProgressIndicator( - minHeight: 10.0, - value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, - ), - ), - Text( - " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12), - ), - ], - ), - ), + if (Platform.isIOS) buildiCloudDownloadProgerssBar(), + buildUploadProgressBar(), Padding( padding: const EdgeInsets.only(top: 8.0), child: buildAssetInfoTable(), diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index 2bdb3a5dd..86a35af2a 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/mobile/lib/modules/backup/views/backup_controller_page.dart @@ -1,33 +1,21 @@ import 'dart:io'; +import 'dart:math'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; -import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; -import 'package:immich_mobile/modules/backup/services/backup_verification.service.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/backup/models/backup_state.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart'; -import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -import 'package:immich_mobile/shared/ui/immich_toast.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; class BackupControllerPage extends HookConsumerWidget { const BackupControllerPage({Key? key}) : super(key: key); @@ -35,14 +23,8 @@ class BackupControllerPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { BackUpState backupState = ref.watch(backupProvider); - final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings; - final settingsService = ref.watch(appSettingsServiceProvider); - final showBackupFix = Platform.isAndroid && - settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting); final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; - final appRefreshDisabled = - Platform.isIOS && settings?.appRefreshEnabled != true; bool hasExclusiveAccess = backupState.backupProgress != BackUpProgressEnum.inBackground; bool shouldBackup = backupState.allUniqueAssets.length - @@ -51,7 +33,6 @@ class BackupControllerPage extends HookConsumerWidget { !hasExclusiveAccess ? false : true; - final checkInProgress = useState(false); useEffect( () { @@ -75,426 +56,6 @@ class BackupControllerPage extends HookConsumerWidget { [], ); - Future performDeletion(List assets) async { - try { - checkInProgress.value = true; - ImmichToast.show( - context: context, - msg: "Deleting ${assets.length} assets on the server...", - ); - await ref - .read(assetProvider.notifier) - .deleteAssets(assets, force: true); - ImmichToast.show( - context: context, - msg: "Deleted ${assets.length} assets on the server. " - "You can now start a manual backup", - toastType: ToastType.success, - ); - } finally { - checkInProgress.value = false; - } - } - - void performBackupCheck() async { - try { - checkInProgress.value = true; - if (backupState.allUniqueAssets.length > - backupState.selectedAlbumsBackupAssetsIds.length) { - ImmichToast.show( - context: context, - msg: "Backup all assets before starting this check!", - toastType: ToastType.error, - ); - return; - } - final connection = await Connectivity().checkConnectivity(); - if (connection != ConnectivityResult.wifi) { - ImmichToast.show( - context: context, - msg: "Make sure to be connected to unmetered Wi-Fi", - toastType: ToastType.error, - ); - return; - } - WakelockPlus.enable(); - const limit = 100; - final toDelete = await ref - .read(backupVerificationServiceProvider) - .findWronglyBackedUpAssets(limit: limit); - if (toDelete.isEmpty) { - ImmichToast.show( - context: context, - msg: "Did not find any corrupt asset backups!", - toastType: ToastType.success, - ); - } else { - await showDialog( - context: context, - builder: (context) => ConfirmDialog( - onOk: () => performDeletion(toDelete), - title: "Corrupt backups!", - ok: "Delete", - content: - "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " - "Run the check again to find more.\n" - "Do you want to delete the corrupt asset backups now?", - ), - ); - } - } finally { - WakelockPlus.disable(); - checkInProgress.value = false; - } - } - - Widget buildCheckCorruptBackups() { - return ListTile( - leading: Icon( - Icons.warning_rounded, - color: context.primaryColor, - ), - title: const Text( - "Check for corrupt asset backups", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - isThreeLine: true, - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text("Run this check only over Wi-Fi and once all assets " - "have been backed-up. The procedure might take a few minutes."), - ElevatedButton( - onPressed: checkInProgress.value ? null : performBackupCheck, - child: checkInProgress.value - ? const CircularProgressIndicator() - : const Text("Perform check"), - ), - ], - ), - ); - } - - ListTile buildAutoBackupController() { - final isAutoBackup = backupState.autoBackup; - final backUpOption = isAutoBackup - ? "backup_controller_page_status_on".tr() - : "backup_controller_page_status_off".tr(); - final backupBtnText = isAutoBackup - ? "backup_controller_page_turn_off".tr() - : "backup_controller_page_turn_on".tr(); - return ListTile( - isThreeLine: true, - leading: isAutoBackup - ? Icon( - Icons.cloud_done_rounded, - color: context.primaryColor, - ) - : const Icon(Icons.cloud_off_rounded), - title: Text( - backUpOption, - style: context.textTheme.titleSmall, - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isAutoBackup) - const Text( - "backup_controller_page_desc_backup", - style: TextStyle(fontSize: 14), - ).tr(), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: ElevatedButton( - onPressed: () => ref - .read(backupProvider.notifier) - .setAutoBackup(!isAutoBackup), - child: Text( - backupBtnText, - style: context.textTheme.labelLarge?.copyWith( - color: context.isDarkTheme ? Colors.black : Colors.white, - ), - ), - ), - ), - ], - ), - ), - ); - } - - 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( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - 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: () { - context.pop(); - }, - ), - ], - ); - }, - ); - } - - Widget buildBackgroundBackupController() { - final bool isBackgroundEnabled = backupState.backgroundBackup; - final bool isWifiRequired = backupState.backupRequireWifi; - final bool isChargingRequired = backupState.backupRequireCharging; - final Color activeColor = context.primaryColor; - - String formatBackupDelaySliderValue(double v) { - if (v == 0.0) { - return 'setting_notifications_notify_seconds'.tr(args: const ['5']); - } else if (v == 1.0) { - return 'setting_notifications_notify_seconds'.tr(args: const ['30']); - } else if (v == 2.0) { - return 'setting_notifications_notify_minutes'.tr(args: const ['2']); - } else { - return 'setting_notifications_notify_minutes'.tr(args: const ['10']); - } - } - - int backupDelayToMilliseconds(double v) { - if (v == 0.0) { - return 5000; - } else if (v == 1.0) { - return 30000; - } else if (v == 2.0) { - return 120000; - } else { - return 600000; - } - } - - double backupDelayToSliderValue(int ms) { - if (ms == 5000) { - return 0.0; - } else if (ms == 30000) { - return 1.0; - } else if (ms == 120000) { - return 2.0; - } else { - return 3.0; - } - } - - final triggerDelay = - useState(backupDelayToSliderValue(backupState.backupTriggerDelay)); - - 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: context.textTheme.titleSmall, - ).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 && Platform.isAndroid) - SwitchListTile.adaptive( - title: const Text("backup_controller_page_background_wifi") - .tr(), - secondary: Icon( - Icons.wifi, - color: isWifiRequired ? activeColor : null, - ), - dense: true, - activeColor: activeColor, - value: isWifiRequired, - onChanged: (isChecked) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - requireWifi: isChecked, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ), - if (isBackgroundEnabled) - SwitchListTile.adaptive( - 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: (isChecked) => ref - .read(backupProvider.notifier) - .configureBackgroundBackup( - requireCharging: isChecked, - onError: showErrorToUser, - onBatteryInfo: showBatteryOptimizationInfoToUser, - ), - ), - if (isBackgroundEnabled && Platform.isAndroid) - ListTile( - isThreeLine: false, - dense: true, - title: const Text( - 'backup_controller_page_background_delay', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ).tr( - args: [formatBackupDelaySliderValue(triggerDelay.value)], - ), - subtitle: Slider( - value: triggerDelay.value, - onChanged: (double v) => triggerDelay.value = v, - 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: 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: context.textTheme.labelLarge?.copyWith( - color: context.isDarkTheme ? Colors.black : Colors.white, - ), - ).tr(), - ), - ], - ), - ), - if (isBackgroundEnabled && Platform.isIOS) - FutureBuilder( - future: ref - .read(backgroundServiceProvider) - .getIOSBackgroundAppRefreshEnabled(), - builder: (context, snapshot) { - final enabled = snapshot.data; - // If it's not enabled, show them some kind of alert that says - // background refresh is not enabled - if (enabled != null && !enabled) {} - // If it's enabled, no need to bother them - return Container(); - }, - ), - if (Platform.isIOS && isBackgroundEnabled && settings != null) - IosDebugInfoTile( - settings: settings, - ), - ], - ); - } - - Widget buildBackgroundAppRefreshWarning() { - return ListTile( - isThreeLine: true, - leading: const Icon( - Icons.task_outlined, - ), - title: const Text( - 'backup_controller_page_background_app_refresh_disabled_title', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ).tr(), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: const Text( - 'backup_controller_page_background_app_refresh_disabled_content', - ).tr(), - ), - ElevatedButton( - onPressed: () => openAppSettings(), - child: const Text( - 'backup_controller_page_background_app_refresh_enable_button_text', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ).tr(), - ), - ], - ), - ); - } - Widget buildSelectedAlbumName() { var text = "backup_controller_page_backup_selected".tr(); var albums = ref.watch(backupProvider).selectedBackupAlbums; @@ -688,6 +249,18 @@ class BackupControllerPage extends HookConsumerWidget { Icons.arrow_back_ios_rounded, ), ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + onPressed: () => context.autoPush(const BackupOptionsRoute()), + splashRadius: 24, + icon: const Icon( + Icons.settings_outlined, + ), + ), + ), + ], ), body: Padding( padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), @@ -715,22 +288,9 @@ class BackupControllerPage extends HookConsumerWidget { subtitle: "backup_controller_page_remainder_sub".tr(), info: ref.watch(backupProvider).availableAlbums.isEmpty ? "..." - : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", + : "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}", ), const Divider(), - buildAutoBackupController(), - const Divider(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Platform.isIOS - ? (appRefreshDisabled - ? buildBackgroundAppRefreshWarning() - : buildBackgroundBackupController()) - : buildBackgroundBackupController(), - ), - if (showBackupFix) const Divider(), - if (showBackupFix) buildCheckCorruptBackups(), - const Divider(), const CurrentUploadingAssetInfoBox(), if (!hasExclusiveAccess) buildBackgroundBackupInfo(), buildBackupButton(), diff --git a/mobile/lib/modules/backup/views/backup_options_page.dart b/mobile/lib/modules/backup/views/backup_options_page.dart new file mode 100644 index 000000000..e43e246cc --- /dev/null +++ b/mobile/lib/modules/backup/views/backup_options_page.dart @@ -0,0 +1,521 @@ +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; +import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; +import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; +import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; +import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart'; +import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +class BackupOptionsPage extends HookConsumerWidget { + const BackupOptionsPage({Key? key}) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + BackUpState backupState = ref.watch(backupProvider); + final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings; + final settingsService = ref.watch(appSettingsServiceProvider); + final showBackupFix = Platform.isAndroid && + settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting); + final ignoreIcloudAssets = useState( + settingsService.getSetting(AppSettingsEnum.ignoreIcloudAssets), + ); + final appRefreshDisabled = + Platform.isIOS && settings?.appRefreshEnabled != true; + final checkInProgress = useState(false); + + Future performDeletion(List assets) async { + try { + checkInProgress.value = true; + ImmichToast.show( + context: context, + msg: "Deleting ${assets.length} assets on the server...", + ); + await ref + .read(assetProvider.notifier) + .deleteAssets(assets, force: true); + ImmichToast.show( + context: context, + msg: "Deleted ${assets.length} assets on the server. " + "You can now start a manual backup", + toastType: ToastType.success, + ); + } finally { + checkInProgress.value = false; + } + } + + void performBackupCheck() async { + try { + checkInProgress.value = true; + if (backupState.allUniqueAssets.length > + backupState.selectedAlbumsBackupAssetsIds.length) { + ImmichToast.show( + context: context, + msg: "Backup all assets before starting this check!", + toastType: ToastType.error, + ); + return; + } + final connection = await Connectivity().checkConnectivity(); + if (connection != ConnectivityResult.wifi) { + ImmichToast.show( + context: context, + msg: "Make sure to be connected to unmetered Wi-Fi", + toastType: ToastType.error, + ); + return; + } + WakelockPlus.enable(); + const limit = 100; + final toDelete = await ref + .read(backupVerificationServiceProvider) + .findWronglyBackedUpAssets(limit: limit); + if (toDelete.isEmpty) { + ImmichToast.show( + context: context, + msg: "Did not find any corrupt asset backups!", + toastType: ToastType.success, + ); + } else { + await showDialog( + context: context, + builder: (context) => ConfirmDialog( + onOk: () => performDeletion(toDelete), + title: "Corrupt backups!", + ok: "Delete", + content: + "Found ${toDelete.length} (max $limit at once) corrupt asset backups. " + "Run the check again to find more.\n" + "Do you want to delete the corrupt asset backups now?", + ), + ); + } + } finally { + WakelockPlus.disable(); + checkInProgress.value = false; + } + } + + Widget buildCheckCorruptBackups() { + return ListTile( + leading: Icon( + Icons.warning_rounded, + color: context.primaryColor, + ), + title: const Text( + "Check for corrupt asset backups", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + isThreeLine: true, + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Run this check only over Wi-Fi and once all assets " + "have been backed-up. The procedure might take a few minutes."), + ElevatedButton( + onPressed: checkInProgress.value ? null : performBackupCheck, + child: checkInProgress.value + ? const CircularProgressIndicator() + : const Text("Perform check"), + ), + ], + ), + ); + } + + 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( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + 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: () { + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget buildBackgroundBackupController() { + final bool isBackgroundEnabled = backupState.backgroundBackup; + final bool isWifiRequired = backupState.backupRequireWifi; + final bool isChargingRequired = backupState.backupRequireCharging; + final Color activeColor = context.primaryColor; + + String formatBackupDelaySliderValue(double v) { + if (v == 0.0) { + return 'setting_notifications_notify_seconds'.tr(args: const ['5']); + } else if (v == 1.0) { + return 'setting_notifications_notify_seconds'.tr(args: const ['30']); + } else if (v == 2.0) { + return 'setting_notifications_notify_minutes'.tr(args: const ['2']); + } else { + return 'setting_notifications_notify_minutes'.tr(args: const ['10']); + } + } + + int backupDelayToMilliseconds(double v) { + if (v == 0.0) { + return 5000; + } else if (v == 1.0) { + return 30000; + } else if (v == 2.0) { + return 120000; + } else { + return 600000; + } + } + + double backupDelayToSliderValue(int ms) { + if (ms == 5000) { + return 0.0; + } else if (ms == 30000) { + return 1.0; + } else if (ms == 120000) { + return 2.0; + } else { + return 3.0; + } + } + + final triggerDelay = + useState(backupDelayToSliderValue(backupState.backupTriggerDelay)); + + 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: context.textTheme.titleSmall, + ).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 && Platform.isAndroid) + SwitchListTile.adaptive( + title: const Text("backup_controller_page_background_wifi") + .tr(), + secondary: Icon( + Icons.wifi, + color: isWifiRequired ? activeColor : null, + ), + dense: true, + activeColor: activeColor, + value: isWifiRequired, + onChanged: (isChecked) => ref + .read(backupProvider.notifier) + .configureBackgroundBackup( + requireWifi: isChecked, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, + ), + ), + if (isBackgroundEnabled) + SwitchListTile.adaptive( + 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: (isChecked) => ref + .read(backupProvider.notifier) + .configureBackgroundBackup( + requireCharging: isChecked, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, + ), + ), + if (isBackgroundEnabled && Platform.isAndroid) + ListTile( + isThreeLine: false, + dense: true, + title: const Text( + 'backup_controller_page_background_delay', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr( + args: [formatBackupDelaySliderValue(triggerDelay.value)], + ), + subtitle: Slider( + value: triggerDelay.value, + onChanged: (double v) => triggerDelay.value = v, + 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: 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: context.textTheme.labelLarge?.copyWith( + color: context.isDarkTheme ? Colors.black : Colors.white, + ), + ).tr(), + ), + ], + ), + ), + if (isBackgroundEnabled && Platform.isIOS) + FutureBuilder( + future: ref + .read(backgroundServiceProvider) + .getIOSBackgroundAppRefreshEnabled(), + builder: (context, snapshot) { + final enabled = snapshot.data; + // If it's not enabled, show them some kind of alert that says + // background refresh is not enabled + if (enabled != null && !enabled) {} + // If it's enabled, no need to bother them + return Container(); + }, + ), + if (Platform.isIOS && isBackgroundEnabled && settings != null) + IosDebugInfoTile( + settings: settings, + ), + ], + ); + } + + Widget buildBackgroundAppRefreshWarning() { + return ListTile( + isThreeLine: true, + leading: const Icon( + Icons.task_outlined, + ), + title: const Text( + 'backup_controller_page_background_app_refresh_disabled_title', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ).tr(), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: const Text( + 'backup_controller_page_background_app_refresh_disabled_content', + ).tr(), + ), + ElevatedButton( + onPressed: () => openAppSettings(), + child: const Text( + 'backup_controller_page_background_app_refresh_enable_button_text', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ).tr(), + ), + ], + ), + ); + } + + ListTile buildAutoBackupController() { + final isAutoBackup = backupState.autoBackup; + final backUpOption = isAutoBackup + ? "backup_controller_page_status_on".tr() + : "backup_controller_page_status_off".tr(); + final backupBtnText = isAutoBackup + ? "backup_controller_page_turn_off".tr() + : "backup_controller_page_turn_on".tr(); + return ListTile( + isThreeLine: true, + leading: isAutoBackup + ? Icon( + Icons.cloud_done_rounded, + color: context.primaryColor, + ) + : const Icon(Icons.cloud_off_rounded), + title: Text( + backUpOption, + style: context.textTheme.titleSmall, + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isAutoBackup) + const Text( + "backup_controller_page_desc_backup", + ).tr(), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ElevatedButton( + onPressed: () => ref + .read(backupProvider.notifier) + .setAutoBackup(!isAutoBackup), + child: Text( + backupBtnText, + style: context.textTheme.labelLarge?.copyWith( + color: context.isDarkTheme ? Colors.black : Colors.white, + ), + ), + ), + ), + ], + ), + ), + ); + } + + void switchChanged(bool value) { + settingsService.setSetting(AppSettingsEnum.ignoreIcloudAssets, value); + ignoreIcloudAssets.value = value; + ref.invalidate(appSettingsServiceProvider); + } + + buildIgnoreIcloudAssetSetting() { + return [ + const Divider(), + SwitchListTile.adaptive( + title: const Text( + "Ignore iCloud photos", + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: const Text( + "Photos that are stored on iCloud will not be uploaded to the Immich server", + ), + value: ignoreIcloudAssets.value, + onChanged: switchChanged, + ), + ]; + } + + return Scaffold( + appBar: AppBar( + elevation: 0, + title: const Text( + "Backup options", + ), + leading: IconButton( + onPressed: () { + context.autoPop(true); + }, + splashRadius: 24, + icon: const Icon( + Icons.arrow_back_ios_rounded, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 32.0), + child: ListView( + children: [ + buildAutoBackupController(), + const Divider(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: Platform.isIOS + ? (appRefreshDisabled + ? buildBackgroundAppRefreshWarning() + : buildBackgroundBackupController()) + : buildBackgroundBackupController(), + ), + if (Platform.isIOS) ...buildIgnoreIcloudAssetSetting(), + if (showBackupFix) const Divider(), + if (showBackupFix) buildCheckCorruptBackups(), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart index 657e916bf..509dd5a93 100644 --- a/mobile/lib/modules/settings/services/app_settings.service.dart +++ b/mobile/lib/modules/settings/services/app_settings.service.dart @@ -51,6 +51,7 @@ enum AppSettingsEnum { mapIncludeArchived(StoreKey.mapIncludeArchived, null, false), mapRelativeDate(StoreKey.mapRelativeDate, null, 0), allowSelfSignedSSLCert(StoreKey.selfSignedCert, null, false), + ignoreIcloudAssets(StoreKey.ignoreIcloudAssets, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 0773f7aa0..4ac13ce94 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/album/views/album_viewer_page.dart'; import 'package:immich_mobile/modules/album/views/asset_selection_page.dart'; import 'package:immich_mobile/modules/album/views/create_album_page.dart'; import 'package:immich_mobile/modules/album/views/library_page.dart'; +import 'package:immich_mobile/modules/backup/views/backup_options_page.dart'; import 'package:immich_mobile/modules/map/ui/map_location_picker.dart'; import 'package:immich_mobile/modules/map/views/map_page.dart'; import 'package:immich_mobile/modules/memories/models/memory.dart'; @@ -178,6 +179,7 @@ part 'router.gr.dart'; page: MapLocationPickerPage, guards: [AuthGuard, DuplicateGuard], ), + AutoRoute(page: BackupOptionsPage, guards: [AuthGuard, DuplicateGuard]), ], ) class AppRouter extends _$AppRouter { diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index af61a2b90..79684fd02 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -371,6 +371,12 @@ class _$AppRouter extends RootStackRouter { barrierDismissible: false, ); }, + BackupOptionsRoute.name: (routeData) { + return MaterialPageX( + routeData: routeData, + child: const BackupOptionsPage(), + ); + }, HomeRoute.name: (routeData) { return MaterialPageX( routeData: routeData, @@ -723,6 +729,14 @@ class _$AppRouter extends RootStackRouter { duplicateGuard, ], ), + RouteConfig( + BackupOptionsRoute.name, + path: '/backup-options-page', + guards: [ + authGuard, + duplicateGuard, + ], + ), ]; } @@ -1664,6 +1678,18 @@ class MapLocationPickerRouteArgs { } } +/// generated route for +/// [BackupOptionsPage] +class BackupOptionsRoute extends PageRouteInfo { + const BackupOptionsRoute() + : super( + BackupOptionsRoute.name, + path: '/backup-options-page', + ); + + static const String name = 'BackupOptionsRoute'; +} + /// generated route for /// [HomePage] class HomeRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart index 8a186af9a..7887f7255 100644 --- a/mobile/lib/shared/models/store.dart +++ b/mobile/lib/shared/models/store.dart @@ -182,6 +182,7 @@ enum StoreKey { mapRelativeDate(119, type: int), selfSignedCert(120, type: bool), mapIncludeArchived(121, type: bool), + ignoreIcloudAssets(122, type: bool), ; const StoreKey(