mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feature(mobile): no longer wait for background backup in settings (#1984)
* feature(mobile): no longer wait for background backup in settings migrate all Hive boxes required for the backup process to Isar * add final modifier
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							f56eaae019
						
					
				
				
					commit
					05cf5d57a9
				
			| @@ -9,6 +9,8 @@ import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/locales.dart'; | ||||
| import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| @@ -104,6 +106,8 @@ Future<Isar> loadDb() async { | ||||
|       AssetSchema, | ||||
|       AlbumSchema, | ||||
|       UserSchema, | ||||
|       BackupAlbumSchema, | ||||
|       DuplicatedAssetSchema, | ||||
|     ], | ||||
|     directory: dir.path, | ||||
|     maxSizeMiB: 256, | ||||
| @@ -156,9 +160,11 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|  | ||||
|         ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); | ||||
|  | ||||
|         ref.watch(notificationPermissionProvider.notifier) | ||||
|         ref | ||||
|             .watch(notificationPermissionProvider.notifier) | ||||
|             .getNotificationPermission(); | ||||
|         ref.watch(galleryPermissionNotifier.notifier) | ||||
|         ref | ||||
|             .watch(galleryPermissionNotifier.notifier) | ||||
|             .getGalleryPermissionStatus(); | ||||
|  | ||||
|         ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); | ||||
|   | ||||
| @@ -2,11 +2,9 @@ import 'dart:async'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/foundation.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/background.service.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| @@ -24,27 +22,27 @@ final albumServiceProvider = Provider( | ||||
|   (ref) => AlbumService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|     ref.watch(userServiceProvider), | ||||
|     ref.watch(backgroundServiceProvider), | ||||
|     ref.watch(syncServiceProvider), | ||||
|     ref.watch(dbProvider), | ||||
|     ref.watch(backupServiceProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class AlbumService { | ||||
|   final ApiService _apiService; | ||||
|   final UserService _userService; | ||||
|   final BackgroundService _backgroundService; | ||||
|   final SyncService _syncService; | ||||
|   final Isar _db; | ||||
|   final BackupService _backupService; | ||||
|   Completer<bool> _localCompleter = Completer()..complete(false); | ||||
|   Completer<bool> _remoteCompleter = Completer()..complete(false); | ||||
|  | ||||
|   AlbumService( | ||||
|     this._apiService, | ||||
|     this._userService, | ||||
|     this._backgroundService, | ||||
|     this._syncService, | ||||
|     this._db, | ||||
|     this._backupService, | ||||
|   ); | ||||
|  | ||||
|   /// Checks all selected device albums for changes of albums and their assets | ||||
| @@ -58,13 +56,11 @@ class AlbumService { | ||||
|     final Stopwatch sw = Stopwatch()..start(); | ||||
|     bool changes = false; | ||||
|     try { | ||||
|       if (!await _backgroundService.hasAccess) { | ||||
|         return false; | ||||
|       } | ||||
|       final HiveBackupAlbums? infos = | ||||
|           (await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox)) | ||||
|               .get(backupInfoKey); | ||||
|       if (infos == null) { | ||||
|       final List<String> excludedIds = | ||||
|           await _backupService.excludedAlbumsQuery().idProperty().findAll(); | ||||
|       final List<String> selectedIds = | ||||
|           await _backupService.selectedAlbumsQuery().idProperty().findAll(); | ||||
|       if (selectedIds.isEmpty) { | ||||
|         return false; | ||||
|       } | ||||
|       final List<AssetPathEntity> onDevice = | ||||
| @@ -72,11 +68,11 @@ class AlbumService { | ||||
|         hasAll: true, | ||||
|         filterOption: FilterOptionGroup(containsPathModified: true), | ||||
|       ); | ||||
|       if (infos.excludedAlbumsIds.isNotEmpty) { | ||||
|       if (excludedIds.isNotEmpty) { | ||||
|         // remove all excluded albums | ||||
|         onDevice.removeWhere((e) => infos.excludedAlbumsIds.contains(e.id)); | ||||
|         onDevice.removeWhere((e) => excludedIds.contains(e.id)); | ||||
|       } | ||||
|       final hasAll = infos.selectedAlbumIds | ||||
|       final hasAll = selectedIds | ||||
|           .map((id) => onDevice.firstWhereOrNull((a) => a.id == id)) | ||||
|           .whereNotNull() | ||||
|           .any((a) => a.isAll); | ||||
| @@ -85,7 +81,7 @@ class AlbumService { | ||||
|         onDevice.removeWhere((e) => e.isAll); | ||||
|       } else { | ||||
|         // keep only the explicitly selected albums | ||||
|         onDevice.removeWhere((e) => !infos.selectedAlbumIds.contains(e.id)); | ||||
|         onDevice.removeWhere((e) => !selectedIds.contains(e.id)); | ||||
|       } | ||||
|       changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice); | ||||
|     } finally { | ||||
|   | ||||
| @@ -4,21 +4,25 @@ import 'dart:io'; | ||||
| import 'dart:isolate'; | ||||
| import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; | ||||
| import 'package:cancellation_token_http/http.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| 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/main.dart'; | ||||
| import 'package:immich_mobile/modules/backup/background_service/localization.dart'; | ||||
| 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/error_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| 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/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:immich_mobile/utils/diff.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:path_provider_ios/path_provider_ios.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| @@ -51,10 +55,6 @@ class BackgroundService { | ||||
|       _Throttle(_updateProgress, notifyInterval); | ||||
|   late final _Throttle _throttledDetailNotify = | ||||
|       _Throttle(_updateDetailProgress, notifyInterval); | ||||
|   Completer<bool> _hasAccessCompleter = Completer(); | ||||
|   late Future<bool> _hasAccess = _hasAccessCompleter.future; | ||||
|  | ||||
|   Future<bool> get hasAccess => _hasAccess; | ||||
|  | ||||
|   bool get isBackgroundInitialized { | ||||
|     return _isBackgroundInitialized; | ||||
| @@ -194,11 +194,6 @@ class BackgroundService { | ||||
|       debugPrint("WARNING: [acquireLock] called more than once"); | ||||
|       return true; | ||||
|     } | ||||
|     if (_hasAccessCompleter.isCompleted) { | ||||
|       debugPrint("WARNING: [acquireLock] _hasAccessCompleter is completed"); | ||||
|       _hasAccessCompleter = Completer(); | ||||
|       _hasAccess = _hasAccessCompleter.future; | ||||
|     } | ||||
|     final int lockTime = Timeline.now; | ||||
|     _wantsLockTime = lockTime; | ||||
|     final ReceivePort rp = ReceivePort(_portNameLock); | ||||
| @@ -217,7 +212,6 @@ class BackgroundService { | ||||
|     } | ||||
|     _hasLock = true; | ||||
|     rp.listen(_heartbeatListener); | ||||
|     _hasAccessCompleter.complete(true); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
| @@ -267,8 +261,6 @@ class BackgroundService { | ||||
|   void releaseLock() { | ||||
|     _wantsLockTime = 0; | ||||
|     if (_hasLock) { | ||||
|       _hasAccessCompleter = Completer(); | ||||
|       _hasAccess = _hasAccessCompleter.future; | ||||
|       IsolateNameServer.removePortNameMapping(_portNameLock); | ||||
|       _waitingIsolate?.send(true); | ||||
|       _waitingIsolate = null; | ||||
| @@ -339,29 +331,24 @@ class BackgroundService { | ||||
|   } | ||||
|  | ||||
|   Future<bool> _onAssetsChanged() async { | ||||
|     final Isar db = await loadDb(); | ||||
|     await Hive.initFlutter(); | ||||
|  | ||||
|     Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||
|     Hive.registerAdapter(HiveBackupAlbumsAdapter()); | ||||
|     Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); | ||||
|  | ||||
|     await Future.wait([ | ||||
|       Hive.openBox(userInfoBox), | ||||
|       Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox), | ||||
|       Hive.openBox(userSettingInfoBox), | ||||
|       Hive.openBox(backgroundBackupInfoBox), | ||||
|       Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox), | ||||
|       Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox), | ||||
|     ]); | ||||
|     ApiService apiService = ApiService(); | ||||
|     apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey)); | ||||
|     BackupService backupService = BackupService(apiService); | ||||
|     BackupService backupService = BackupService(apiService, db); | ||||
|     AppSettingsService settingsService = AppSettingsService(); | ||||
|  | ||||
|     final Box<HiveBackupAlbums> box = | ||||
|         Hive.box<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|     final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey); | ||||
|     if (backupAlbumInfo == null) { | ||||
|     final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); | ||||
|     final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); | ||||
|     if (selectedAlbums.isEmpty) { | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
| @@ -371,18 +358,37 @@ class BackgroundService { | ||||
|       final bool backupOk = await _runBackup( | ||||
|         backupService, | ||||
|         settingsService, | ||||
|         backupAlbumInfo, | ||||
|         selectedAlbums, | ||||
|         excludedAlbums, | ||||
|       ); | ||||
|       if (backupOk) { | ||||
|         await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince); | ||||
|         await box.put( | ||||
|           backupInfoKey, | ||||
|           backupAlbumInfo, | ||||
|         await Store.delete(StoreKey.backupFailedSince); | ||||
|         final backupAlbums = [...selectedAlbums, ...excludedAlbums]; | ||||
|         backupAlbums.sortBy((e) => e.id); | ||||
|         db.writeTxnSync(() { | ||||
|           final dbAlbums = db.backupAlbums.where().sortById().findAllSync(); | ||||
|           final List<int> toDelete = []; | ||||
|           final List<BackupAlbum> toUpsert = []; | ||||
|           // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state | ||||
|           diffSortedListsSync( | ||||
|             dbAlbums, | ||||
|             backupAlbums, | ||||
|             compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), | ||||
|             both: (BackupAlbum a, BackupAlbum b) { | ||||
|               a.lastBackup = a.lastBackup.isAfter(b.lastBackup) | ||||
|                   ? a.lastBackup | ||||
|                   : b.lastBackup; | ||||
|               toUpsert.add(a); | ||||
|               return true; | ||||
|             }, | ||||
|             onlyFirst: (BackupAlbum a) => toUpsert.add(a), | ||||
|             onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), | ||||
|           ); | ||||
|       } else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) == | ||||
|           null) { | ||||
|         Hive.box(backgroundBackupInfoBox) | ||||
|             .put(backupFailedSince, DateTime.now()); | ||||
|           db.backupAlbums.deleteAllSync(toDelete); | ||||
|           db.backupAlbums.putAllSync(toUpsert); | ||||
|         }); | ||||
|       } else if (Store.get(StoreKey.backupFailedSince) == null) { | ||||
|         Store.put(StoreKey.backupFailedSince, DateTime.now()); | ||||
|         return false; | ||||
|       } | ||||
|       // Android should check for new assets added while performing backup | ||||
| @@ -395,7 +401,8 @@ class BackgroundService { | ||||
|   Future<bool> _runBackup( | ||||
|     BackupService backupService, | ||||
|     AppSettingsService settingsService, | ||||
|     HiveBackupAlbums backupAlbumInfo, | ||||
|     List<BackupAlbum> selectedAlbums, | ||||
|     List<BackupAlbum> excludedAlbums, | ||||
|   ) async { | ||||
|     _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); | ||||
|     final bool notifyTotalProgress = settingsService | ||||
| @@ -407,8 +414,10 @@ class BackgroundService { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     List<AssetEntity> toUpload = | ||||
|         await backupService.buildUploadCandidates(backupAlbumInfo); | ||||
|     List<AssetEntity> toUpload = await backupService.buildUploadCandidates( | ||||
|       selectedAlbums, | ||||
|       excludedAlbums, | ||||
|     ); | ||||
|  | ||||
|     try { | ||||
|       toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); | ||||
| @@ -520,8 +529,7 @@ class BackgroundService { | ||||
|     } else if (value == 5) { | ||||
|       return false; | ||||
|     } | ||||
|     final DateTime? failedSince = | ||||
|         Hive.box(backgroundBackupInfoBox).get(backupFailedSince); | ||||
|     final DateTime? failedSince = Store.get(StoreKey.backupFailedSince); | ||||
|     if (failedSince == null) { | ||||
|       return false; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										22
									
								
								mobile/lib/modules/backup/models/backup_album.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								mobile/lib/modules/backup/models/backup_album.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import 'package:immich_mobile/utils/hash.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
|  | ||||
| part 'backup_album.model.g.dart'; | ||||
|  | ||||
| @Collection(inheritance: false) | ||||
| class BackupAlbum { | ||||
|   String id; | ||||
|   DateTime lastBackup; | ||||
|   @Enumerated(EnumType.ordinal) | ||||
|   BackupSelection selection; | ||||
|  | ||||
|   BackupAlbum(this.id, this.lastBackup, this.selection); | ||||
|  | ||||
|   Id get isarId => fastHash(id); | ||||
| } | ||||
|  | ||||
| enum BackupSelection { | ||||
|   none, | ||||
|   select, | ||||
|   exclude; | ||||
| } | ||||
							
								
								
									
										653
									
								
								mobile/lib/modules/backup/models/backup_album.model.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										653
									
								
								mobile/lib/modules/backup/models/backup_album.model.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,653 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'backup_album.model.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // IsarCollectionGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // coverage:ignore-file | ||||
| // ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters | ||||
|  | ||||
| extension GetBackupAlbumCollection on Isar { | ||||
|   IsarCollection<BackupAlbum> get backupAlbums => this.collection(); | ||||
| } | ||||
|  | ||||
| const BackupAlbumSchema = CollectionSchema( | ||||
|   name: r'BackupAlbum', | ||||
|   id: 8308487201128361847, | ||||
|   properties: { | ||||
|     r'id': PropertySchema( | ||||
|       id: 0, | ||||
|       name: r'id', | ||||
|       type: IsarType.string, | ||||
|     ), | ||||
|     r'lastBackup': PropertySchema( | ||||
|       id: 1, | ||||
|       name: r'lastBackup', | ||||
|       type: IsarType.dateTime, | ||||
|     ), | ||||
|     r'selection': PropertySchema( | ||||
|       id: 2, | ||||
|       name: r'selection', | ||||
|       type: IsarType.byte, | ||||
|       enumMap: _BackupAlbumselectionEnumValueMap, | ||||
|     ) | ||||
|   }, | ||||
|   estimateSize: _backupAlbumEstimateSize, | ||||
|   serialize: _backupAlbumSerialize, | ||||
|   deserialize: _backupAlbumDeserialize, | ||||
|   deserializeProp: _backupAlbumDeserializeProp, | ||||
|   idName: r'isarId', | ||||
|   indexes: {}, | ||||
|   links: {}, | ||||
|   embeddedSchemas: {}, | ||||
|   getId: _backupAlbumGetId, | ||||
|   getLinks: _backupAlbumGetLinks, | ||||
|   attach: _backupAlbumAttach, | ||||
|   version: '3.0.5', | ||||
| ); | ||||
|  | ||||
| int _backupAlbumEstimateSize( | ||||
|   BackupAlbum object, | ||||
|   List<int> offsets, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   var bytesCount = offsets.last; | ||||
|   bytesCount += 3 + object.id.length * 3; | ||||
|   return bytesCount; | ||||
| } | ||||
|  | ||||
| void _backupAlbumSerialize( | ||||
|   BackupAlbum object, | ||||
|   IsarWriter writer, | ||||
|   List<int> offsets, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   writer.writeString(offsets[0], object.id); | ||||
|   writer.writeDateTime(offsets[1], object.lastBackup); | ||||
|   writer.writeByte(offsets[2], object.selection.index); | ||||
| } | ||||
|  | ||||
| BackupAlbum _backupAlbumDeserialize( | ||||
|   Id id, | ||||
|   IsarReader reader, | ||||
|   List<int> offsets, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   final object = BackupAlbum( | ||||
|     reader.readString(offsets[0]), | ||||
|     reader.readDateTime(offsets[1]), | ||||
|     _BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ?? | ||||
|         BackupSelection.none, | ||||
|   ); | ||||
|   return object; | ||||
| } | ||||
|  | ||||
| P _backupAlbumDeserializeProp<P>( | ||||
|   IsarReader reader, | ||||
|   int propertyId, | ||||
|   int offset, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   switch (propertyId) { | ||||
|     case 0: | ||||
|       return (reader.readString(offset)) as P; | ||||
|     case 1: | ||||
|       return (reader.readDateTime(offset)) as P; | ||||
|     case 2: | ||||
|       return (_BackupAlbumselectionValueEnumMap[ | ||||
|               reader.readByteOrNull(offset)] ?? | ||||
|           BackupSelection.none) as P; | ||||
|     default: | ||||
|       throw IsarError('Unknown property with id $propertyId'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| const _BackupAlbumselectionEnumValueMap = { | ||||
|   'none': 0, | ||||
|   'select': 1, | ||||
|   'exclude': 2, | ||||
| }; | ||||
| const _BackupAlbumselectionValueEnumMap = { | ||||
|   0: BackupSelection.none, | ||||
|   1: BackupSelection.select, | ||||
|   2: BackupSelection.exclude, | ||||
| }; | ||||
|  | ||||
| Id _backupAlbumGetId(BackupAlbum object) { | ||||
|   return object.isarId; | ||||
| } | ||||
|  | ||||
| List<IsarLinkBase<dynamic>> _backupAlbumGetLinks(BackupAlbum object) { | ||||
|   return []; | ||||
| } | ||||
|  | ||||
| void _backupAlbumAttach( | ||||
|     IsarCollection<dynamic> col, Id id, BackupAlbum object) {} | ||||
|  | ||||
| extension BackupAlbumQueryWhereSort | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QWhere> { | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhere> anyIsarId() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause(const IdWhereClause.any()); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension BackupAlbumQueryWhere | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QWhereClause> { | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdEqualTo( | ||||
|       Id isarId) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause(IdWhereClause.between( | ||||
|         lower: isarId, | ||||
|         upper: isarId, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdNotEqualTo( | ||||
|       Id isarId) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       if (query.whereSort == Sort.asc) { | ||||
|         return query | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.lessThan(upper: isarId, includeUpper: false), | ||||
|             ) | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.greaterThan(lower: isarId, includeLower: false), | ||||
|             ); | ||||
|       } else { | ||||
|         return query | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.greaterThan(lower: isarId, includeLower: false), | ||||
|             ) | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.lessThan(upper: isarId, includeUpper: false), | ||||
|             ); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdGreaterThan( | ||||
|       Id isarId, | ||||
|       {bool include = false}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause( | ||||
|         IdWhereClause.greaterThan(lower: isarId, includeLower: include), | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdLessThan( | ||||
|       Id isarId, | ||||
|       {bool include = false}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause( | ||||
|         IdWhereClause.lessThan(upper: isarId, includeUpper: include), | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdBetween( | ||||
|     Id lowerIsarId, | ||||
|     Id upperIsarId, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause(IdWhereClause.between( | ||||
|         lower: lowerIsarId, | ||||
|         includeLower: includeLower, | ||||
|         upper: upperIsarId, | ||||
|         includeUpper: includeUpper, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension BackupAlbumQueryFilter | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> { | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idEqualTo( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idGreaterThan( | ||||
|     String value, { | ||||
|     bool include = false, | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         include: include, | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idLessThan( | ||||
|     String value, { | ||||
|     bool include = false, | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.lessThan( | ||||
|         include: include, | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idBetween( | ||||
|     String lower, | ||||
|     String upper, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.between( | ||||
|         property: r'id', | ||||
|         lower: lower, | ||||
|         includeLower: includeLower, | ||||
|         upper: upper, | ||||
|         includeUpper: includeUpper, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idStartsWith( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.startsWith( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idEndsWith( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.endsWith( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idContains( | ||||
|       String value, | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.contains( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idMatches( | ||||
|       String pattern, | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.matches( | ||||
|         property: r'id', | ||||
|         wildcard: pattern, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idIsEmpty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'id', | ||||
|         value: '', | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idIsNotEmpty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         property: r'id', | ||||
|         value: '', | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> isarIdEqualTo( | ||||
|       Id value) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'isarId', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       isarIdGreaterThan( | ||||
|     Id value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         include: include, | ||||
|         property: r'isarId', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> isarIdLessThan( | ||||
|     Id value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.lessThan( | ||||
|         include: include, | ||||
|         property: r'isarId', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> isarIdBetween( | ||||
|     Id lower, | ||||
|     Id upper, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.between( | ||||
|         property: r'isarId', | ||||
|         lower: lower, | ||||
|         includeLower: includeLower, | ||||
|         upper: upper, | ||||
|         includeUpper: includeUpper, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       lastBackupEqualTo(DateTime value) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'lastBackup', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       lastBackupGreaterThan( | ||||
|     DateTime value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         include: include, | ||||
|         property: r'lastBackup', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       lastBackupLessThan( | ||||
|     DateTime value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.lessThan( | ||||
|         include: include, | ||||
|         property: r'lastBackup', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       lastBackupBetween( | ||||
|     DateTime lower, | ||||
|     DateTime upper, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.between( | ||||
|         property: r'lastBackup', | ||||
|         lower: lower, | ||||
|         includeLower: includeLower, | ||||
|         upper: upper, | ||||
|         includeUpper: includeUpper, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       selectionEqualTo(BackupSelection value) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'selection', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       selectionGreaterThan( | ||||
|     BackupSelection value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         include: include, | ||||
|         property: r'selection', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       selectionLessThan( | ||||
|     BackupSelection value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.lessThan( | ||||
|         include: include, | ||||
|         property: r'selection', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       selectionBetween( | ||||
|     BackupSelection lower, | ||||
|     BackupSelection upper, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.between( | ||||
|         property: r'selection', | ||||
|         lower: lower, | ||||
|         includeLower: includeLower, | ||||
|         upper: upper, | ||||
|         includeUpper: includeUpper, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension BackupAlbumQueryObject | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {} | ||||
|  | ||||
| extension BackupAlbumQueryLinks | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {} | ||||
|  | ||||
| extension BackupAlbumQuerySortBy | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QSortBy> { | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortById() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByIdDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByLastBackup() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'lastBackup', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByLastBackupDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'lastBackup', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortBySelection() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'selection', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortBySelectionDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'selection', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension BackupAlbumQuerySortThenBy | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QSortThenBy> { | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenById() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByIdDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByIsarId() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'isarId', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByIsarIdDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'isarId', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByLastBackup() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'lastBackup', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByLastBackupDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'lastBackup', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenBySelection() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'selection', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenBySelectionDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'selection', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension BackupAlbumQueryWhereDistinct | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> { | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctById( | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'id', caseSensitive: caseSensitive); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctByLastBackup() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'lastBackup'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctBySelection() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'selection'); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension BackupAlbumQueryProperty | ||||
|     on QueryBuilder<BackupAlbum, BackupAlbum, QQueryProperty> { | ||||
|   QueryBuilder<BackupAlbum, int, QQueryOperations> isarIdProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'isarId'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, String, QQueryOperations> idProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'id'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, DateTime, QQueryOperations> lastBackupProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'lastBackup'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupSelection, QQueryOperations> | ||||
|       selectionProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'selection'); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								mobile/lib/modules/backup/models/duplicated_asset.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								mobile/lib/modules/backup/models/duplicated_asset.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import 'package:immich_mobile/utils/hash.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
|  | ||||
| part 'duplicated_asset.model.g.dart'; | ||||
|  | ||||
| @Collection(inheritance: false) | ||||
| class DuplicatedAsset { | ||||
|   String id; | ||||
|   DuplicatedAsset(this.id); | ||||
|   Id get isarId => fastHash(id); | ||||
| } | ||||
							
								
								
									
										443
									
								
								mobile/lib/modules/backup/models/duplicated_asset.model.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										443
									
								
								mobile/lib/modules/backup/models/duplicated_asset.model.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,443 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'duplicated_asset.model.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // IsarCollectionGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // coverage:ignore-file | ||||
| // ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters | ||||
|  | ||||
| extension GetDuplicatedAssetCollection on Isar { | ||||
|   IsarCollection<DuplicatedAsset> get duplicatedAssets => this.collection(); | ||||
| } | ||||
|  | ||||
| const DuplicatedAssetSchema = CollectionSchema( | ||||
|   name: r'DuplicatedAsset', | ||||
|   id: -2679334728174694496, | ||||
|   properties: { | ||||
|     r'id': PropertySchema( | ||||
|       id: 0, | ||||
|       name: r'id', | ||||
|       type: IsarType.string, | ||||
|     ) | ||||
|   }, | ||||
|   estimateSize: _duplicatedAssetEstimateSize, | ||||
|   serialize: _duplicatedAssetSerialize, | ||||
|   deserialize: _duplicatedAssetDeserialize, | ||||
|   deserializeProp: _duplicatedAssetDeserializeProp, | ||||
|   idName: r'isarId', | ||||
|   indexes: {}, | ||||
|   links: {}, | ||||
|   embeddedSchemas: {}, | ||||
|   getId: _duplicatedAssetGetId, | ||||
|   getLinks: _duplicatedAssetGetLinks, | ||||
|   attach: _duplicatedAssetAttach, | ||||
|   version: '3.0.5', | ||||
| ); | ||||
|  | ||||
| int _duplicatedAssetEstimateSize( | ||||
|   DuplicatedAsset object, | ||||
|   List<int> offsets, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   var bytesCount = offsets.last; | ||||
|   bytesCount += 3 + object.id.length * 3; | ||||
|   return bytesCount; | ||||
| } | ||||
|  | ||||
| void _duplicatedAssetSerialize( | ||||
|   DuplicatedAsset object, | ||||
|   IsarWriter writer, | ||||
|   List<int> offsets, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   writer.writeString(offsets[0], object.id); | ||||
| } | ||||
|  | ||||
| DuplicatedAsset _duplicatedAssetDeserialize( | ||||
|   Id id, | ||||
|   IsarReader reader, | ||||
|   List<int> offsets, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   final object = DuplicatedAsset( | ||||
|     reader.readString(offsets[0]), | ||||
|   ); | ||||
|   return object; | ||||
| } | ||||
|  | ||||
| P _duplicatedAssetDeserializeProp<P>( | ||||
|   IsarReader reader, | ||||
|   int propertyId, | ||||
|   int offset, | ||||
|   Map<Type, List<int>> allOffsets, | ||||
| ) { | ||||
|   switch (propertyId) { | ||||
|     case 0: | ||||
|       return (reader.readString(offset)) as P; | ||||
|     default: | ||||
|       throw IsarError('Unknown property with id $propertyId'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Id _duplicatedAssetGetId(DuplicatedAsset object) { | ||||
|   return object.isarId; | ||||
| } | ||||
|  | ||||
| List<IsarLinkBase<dynamic>> _duplicatedAssetGetLinks(DuplicatedAsset object) { | ||||
|   return []; | ||||
| } | ||||
|  | ||||
| void _duplicatedAssetAttach( | ||||
|     IsarCollection<dynamic> col, Id id, DuplicatedAsset object) {} | ||||
|  | ||||
| extension DuplicatedAssetQueryWhereSort | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QWhere> { | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhere> anyIsarId() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause(const IdWhereClause.any()); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension DuplicatedAssetQueryWhere | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QWhereClause> { | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause> | ||||
|       isarIdEqualTo(Id isarId) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause(IdWhereClause.between( | ||||
|         lower: isarId, | ||||
|         upper: isarId, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause> | ||||
|       isarIdNotEqualTo(Id isarId) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       if (query.whereSort == Sort.asc) { | ||||
|         return query | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.lessThan(upper: isarId, includeUpper: false), | ||||
|             ) | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.greaterThan(lower: isarId, includeLower: false), | ||||
|             ); | ||||
|       } else { | ||||
|         return query | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.greaterThan(lower: isarId, includeLower: false), | ||||
|             ) | ||||
|             .addWhereClause( | ||||
|               IdWhereClause.lessThan(upper: isarId, includeUpper: false), | ||||
|             ); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause> | ||||
|       isarIdGreaterThan(Id isarId, {bool include = false}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause( | ||||
|         IdWhereClause.greaterThan(lower: isarId, includeLower: include), | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause> | ||||
|       isarIdLessThan(Id isarId, {bool include = false}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause( | ||||
|         IdWhereClause.lessThan(upper: isarId, includeUpper: include), | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause> | ||||
|       isarIdBetween( | ||||
|     Id lowerIsarId, | ||||
|     Id upperIsarId, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addWhereClause(IdWhereClause.between( | ||||
|         lower: lowerIsarId, | ||||
|         includeLower: includeLower, | ||||
|         upper: upperIsarId, | ||||
|         includeUpper: includeUpper, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension DuplicatedAssetQueryFilter | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> { | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idEqualTo( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idGreaterThan( | ||||
|     String value, { | ||||
|     bool include = false, | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         include: include, | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idLessThan( | ||||
|     String value, { | ||||
|     bool include = false, | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.lessThan( | ||||
|         include: include, | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idBetween( | ||||
|     String lower, | ||||
|     String upper, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.between( | ||||
|         property: r'id', | ||||
|         lower: lower, | ||||
|         includeLower: includeLower, | ||||
|         upper: upper, | ||||
|         includeUpper: includeUpper, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idStartsWith( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.startsWith( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idEndsWith( | ||||
|     String value, { | ||||
|     bool caseSensitive = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.endsWith( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idContains(String value, {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.contains( | ||||
|         property: r'id', | ||||
|         value: value, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idMatches(String pattern, {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.matches( | ||||
|         property: r'id', | ||||
|         wildcard: pattern, | ||||
|         caseSensitive: caseSensitive, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idIsEmpty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'id', | ||||
|         value: '', | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       idIsNotEmpty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         property: r'id', | ||||
|         value: '', | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       isarIdEqualTo(Id value) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.equalTo( | ||||
|         property: r'isarId', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       isarIdGreaterThan( | ||||
|     Id value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.greaterThan( | ||||
|         include: include, | ||||
|         property: r'isarId', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       isarIdLessThan( | ||||
|     Id value, { | ||||
|     bool include = false, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.lessThan( | ||||
|         include: include, | ||||
|         property: r'isarId', | ||||
|         value: value, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition> | ||||
|       isarIdBetween( | ||||
|     Id lower, | ||||
|     Id upper, { | ||||
|     bool includeLower = true, | ||||
|     bool includeUpper = true, | ||||
|   }) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addFilterCondition(FilterCondition.between( | ||||
|         property: r'isarId', | ||||
|         lower: lower, | ||||
|         includeLower: includeLower, | ||||
|         upper: upper, | ||||
|         includeUpper: includeUpper, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension DuplicatedAssetQueryObject | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {} | ||||
|  | ||||
| extension DuplicatedAssetQueryLinks | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {} | ||||
|  | ||||
| extension DuplicatedAssetQuerySortBy | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QSortBy> { | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> sortById() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> sortByIdDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension DuplicatedAssetQuerySortThenBy | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QSortThenBy> { | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenById() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenByIdDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'id', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenByIsarId() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'isarId', Sort.asc); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> | ||||
|       thenByIsarIdDesc() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addSortBy(r'isarId', Sort.desc); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension DuplicatedAssetQueryWhereDistinct | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QDistinct> { | ||||
|   QueryBuilder<DuplicatedAsset, DuplicatedAsset, QDistinct> distinctById( | ||||
|       {bool caseSensitive = true}) { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addDistinctBy(r'id', caseSensitive: caseSensitive); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension DuplicatedAssetQueryProperty | ||||
|     on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QQueryProperty> { | ||||
|   QueryBuilder<DuplicatedAsset, int, QQueryOperations> isarIdProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'isarId'); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<DuplicatedAsset, String, QQueryOperations> idProperty() { | ||||
|     return QueryBuilder.apply(this, (query) { | ||||
|       return query.addPropertyName(r'id'); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -1,22 +1,26 @@ | ||||
| import 'package:cancellation_token_http/http.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| 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/models/available_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_state.model.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'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/app_state.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/server_info.service.dart'; | ||||
| import 'package:immich_mobile/utils/diff.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| @@ -29,6 +33,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     this._authState, | ||||
|     this._backgroundService, | ||||
|     this._galleryPermissionNotifier, | ||||
|     this._db, | ||||
|     this.ref, | ||||
|   ) : super( | ||||
|           BackUpState( | ||||
| @@ -69,6 +74,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   final AuthenticationState _authState; | ||||
|   final BackgroundService _backgroundService; | ||||
|   final GalleryPermissionNotifier _galleryPermissionNotifier; | ||||
|   final Isar _db; | ||||
|   final Ref ref; | ||||
|  | ||||
|   /// | ||||
| @@ -157,11 +163,13 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|             triggerMaxDelay: state.backupTriggerDelay * 10, | ||||
|           ); | ||||
|       if (success) { | ||||
|         final box = Hive.box(backgroundBackupInfoBox); | ||||
|         await Future.wait([ | ||||
|           box.put(backupRequireWifi, state.backupRequireWifi), | ||||
|           box.put(backupRequireCharging, state.backupRequireCharging), | ||||
|           box.put(backupTriggerDelay, state.backupTriggerDelay), | ||||
|           Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi), | ||||
|           Store.put( | ||||
|             StoreKey.backupRequireCharging, | ||||
|             state.backupRequireCharging, | ||||
|           ), | ||||
|           Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay), | ||||
|         ]); | ||||
|       } else { | ||||
|         state = state.copyWith( | ||||
| @@ -201,16 +209,16 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     for (AssetPathEntity album in albums) { | ||||
|       AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); | ||||
|  | ||||
|       var assetCountInAlbum = await album.assetCountAsync; | ||||
|       final assetCountInAlbum = await album.assetCountAsync; | ||||
|       if (assetCountInAlbum > 0) { | ||||
|         var assetList = | ||||
|         final assetList = | ||||
|             await album.getAssetListRange(start: 0, end: assetCountInAlbum); | ||||
|  | ||||
|         if (assetList.isNotEmpty) { | ||||
|           var thumbnailAsset = assetList.first; | ||||
|           final thumbnailAsset = assetList.first; | ||||
|  | ||||
|           try { | ||||
|             var thumbnailData = await thumbnailAsset | ||||
|             final thumbnailData = await thumbnailAsset | ||||
|                 .thumbnailDataWithSize(const ThumbnailSize(512, 512)); | ||||
|             availableAlbum = | ||||
|                 availableAlbum.copyWith(thumbnailData: thumbnailData); | ||||
| @@ -229,34 +237,17 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|     state = state.copyWith(availableAlbums: availableAlbums); | ||||
|  | ||||
|     // Put persistent storage info into local state of the app | ||||
|     // Get local storage on selected backup album | ||||
|     Box<HiveBackupAlbums> backupAlbumInfoBox = | ||||
|         Hive.box<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|     HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get( | ||||
|       backupInfoKey, | ||||
|       defaultValue: HiveBackupAlbums( | ||||
|         selectedAlbumIds: [], | ||||
|         excludedAlbumsIds: [], | ||||
|         lastSelectedBackupTime: [], | ||||
|         lastExcludedBackupTime: [], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     if (backupAlbumInfo == null) { | ||||
|       log.severe( | ||||
|         "backupAlbumInfo == null", | ||||
|         "Failed to get Hive backup album information", | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|     final List<BackupAlbum> excludedBackupAlbums = | ||||
|         await _backupService.excludedAlbumsQuery().findAll(); | ||||
|     final List<BackupAlbum> selectedBackupAlbums = | ||||
|         await _backupService.selectedAlbumsQuery().findAll(); | ||||
|  | ||||
|     // First time backup - set isAll album is the default one for backup. | ||||
|     if (backupAlbumInfo.selectedAlbumIds.isEmpty) { | ||||
|     if (selectedBackupAlbums.isEmpty) { | ||||
|       log.info("First time backup; setup 'Recent(s)' album as default"); | ||||
|  | ||||
|       // Get album that contains all assets | ||||
|       var list = await PhotoManager.getAssetPathList( | ||||
|       final list = await PhotoManager.getAssetPathList( | ||||
|         hasAll: true, | ||||
|         onlyAll: true, | ||||
|         type: RequestType.common, | ||||
| @@ -267,48 +258,29 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|       } | ||||
|       AssetPathEntity albumHasAllAssets = list.first; | ||||
|  | ||||
|       backupAlbumInfoBox.put( | ||||
|         backupInfoKey, | ||||
|         HiveBackupAlbums( | ||||
|           selectedAlbumIds: [albumHasAllAssets.id], | ||||
|           excludedAlbumsIds: [], | ||||
|           lastSelectedBackupTime: [ | ||||
|             DateTime.fromMillisecondsSinceEpoch(0, isUtc: true) | ||||
|           ], | ||||
|           lastExcludedBackupTime: [], | ||||
|         ), | ||||
|       final ba = BackupAlbum( | ||||
|         albumHasAllAssets.id, | ||||
|         DateTime.fromMillisecondsSinceEpoch(0), | ||||
|         BackupSelection.select, | ||||
|       ); | ||||
|  | ||||
|       backupAlbumInfo = backupAlbumInfoBox.get(backupInfoKey); | ||||
|       await _db.writeTxn(() => _db.backupAlbums.put(ba)); | ||||
|     } | ||||
|  | ||||
|     // Generate AssetPathEntity from id to add to local state | ||||
|     try { | ||||
|       Set<AvailableAlbum> selectedAlbums = {}; | ||||
|       for (var i = 0; i < backupAlbumInfo!.selectedAlbumIds.length; i++) { | ||||
|         var albumAsset = | ||||
|             await AssetPathEntity.fromId(backupAlbumInfo.selectedAlbumIds[i]); | ||||
|       final Set<AvailableAlbum> selectedAlbums = {}; | ||||
|       for (final BackupAlbum ba in selectedBackupAlbums) { | ||||
|         final albumAsset = await AssetPathEntity.fromId(ba.id); | ||||
|         selectedAlbums.add( | ||||
|           AvailableAlbum( | ||||
|             albumEntity: albumAsset, | ||||
|             lastBackup: backupAlbumInfo.lastSelectedBackupTime.length > i | ||||
|                 ? backupAlbumInfo.lastSelectedBackupTime[i] | ||||
|                 : DateTime.fromMillisecondsSinceEpoch(0, isUtc: true), | ||||
|           ), | ||||
|           AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       Set<AvailableAlbum> excludedAlbums = {}; | ||||
|       for (var i = 0; i < backupAlbumInfo.excludedAlbumsIds.length; i++) { | ||||
|         var albumAsset = | ||||
|             await AssetPathEntity.fromId(backupAlbumInfo.excludedAlbumsIds[i]); | ||||
|       final Set<AvailableAlbum> excludedAlbums = {}; | ||||
|       for (final BackupAlbum ba in excludedBackupAlbums) { | ||||
|         final albumAsset = await AssetPathEntity.fromId(ba.id); | ||||
|         excludedAlbums.add( | ||||
|           AvailableAlbum( | ||||
|             albumEntity: albumAsset, | ||||
|             lastBackup: backupAlbumInfo.lastExcludedBackupTime.length > i | ||||
|                 ? backupAlbumInfo.lastExcludedBackupTime[i] | ||||
|                 : DateTime.fromMillisecondsSinceEpoch(0, isUtc: true), | ||||
|           ), | ||||
|           AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup), | ||||
|         ); | ||||
|       } | ||||
|       state = state.copyWith( | ||||
| @@ -328,36 +300,36 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   /// Those assets are unique and are used as the total assets | ||||
|   /// | ||||
|   Future<void> _updateBackupAssetCount() async { | ||||
|     Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds(); | ||||
|     Set<AssetEntity> assetsFromSelectedAlbums = {}; | ||||
|     Set<AssetEntity> assetsFromExcludedAlbums = {}; | ||||
|     final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); | ||||
|     final Set<AssetEntity> assetsFromSelectedAlbums = {}; | ||||
|     final Set<AssetEntity> assetsFromExcludedAlbums = {}; | ||||
|  | ||||
|     for (var album in state.selectedBackupAlbums) { | ||||
|       var assets = await album.albumEntity.getAssetListRange( | ||||
|     for (final album in state.selectedBackupAlbums) { | ||||
|       final assets = await album.albumEntity.getAssetListRange( | ||||
|         start: 0, | ||||
|         end: await album.albumEntity.assetCountAsync, | ||||
|       ); | ||||
|       assetsFromSelectedAlbums.addAll(assets); | ||||
|     } | ||||
|  | ||||
|     for (var album in state.excludedBackupAlbums) { | ||||
|       var assets = await album.albumEntity.getAssetListRange( | ||||
|     for (final album in state.excludedBackupAlbums) { | ||||
|       final assets = await album.albumEntity.getAssetListRange( | ||||
|         start: 0, | ||||
|         end: await album.albumEntity.assetCountAsync, | ||||
|       ); | ||||
|       assetsFromExcludedAlbums.addAll(assets); | ||||
|     } | ||||
|  | ||||
|     Set<AssetEntity> allUniqueAssets = | ||||
|     final Set<AssetEntity> allUniqueAssets = | ||||
|         assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); | ||||
|     var allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); | ||||
|     final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); | ||||
|  | ||||
|     if (allAssetsInDatabase == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Find asset that were backup from selected albums | ||||
|     Set<String> selectedAlbumsBackupAssets = | ||||
|     final Set<String> selectedAlbumsBackupAssets = | ||||
|         Set.from(allUniqueAssets.map((e) => e.id)); | ||||
|  | ||||
|     selectedAlbumsBackupAssets | ||||
| @@ -386,7 +358,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     } | ||||
|  | ||||
|     // Save to persistent storage | ||||
|     _updatePersistentAlbumsSelection(); | ||||
|     await _updatePersistentAlbumsSelection(); | ||||
|  | ||||
|     return; | ||||
|   } | ||||
| @@ -395,7 +367,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   /// which albums are selected or excluded | ||||
|   /// and then update the UI according to those information | ||||
|   Future<void> getBackupInfo() async { | ||||
|     var isEnabled = await _backgroundService.isBackgroundBackupEnabled(); | ||||
|     final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); | ||||
|  | ||||
|     state = state.copyWith(backgroundBackup: isEnabled); | ||||
|  | ||||
| @@ -406,25 +378,38 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Save user selection of selected albums and excluded albums to | ||||
|   /// Hive database | ||||
|   void _updatePersistentAlbumsSelection() { | ||||
|   /// Save user selection of selected albums and excluded albums to database | ||||
|   Future<void> _updatePersistentAlbumsSelection() { | ||||
|     final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); | ||||
|     Box<HiveBackupAlbums> backupAlbumInfoBox = | ||||
|         Hive.box<HiveBackupAlbums>(hiveBackupInfoBox); | ||||
|     backupAlbumInfoBox.put( | ||||
|       backupInfoKey, | ||||
|       HiveBackupAlbums( | ||||
|         selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(), | ||||
|         excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(), | ||||
|         lastSelectedBackupTime: state.selectedBackupAlbums | ||||
|             .map((e) => e.lastBackup ?? epoch) | ||||
|             .toList(), | ||||
|         lastExcludedBackupTime: state.excludedBackupAlbums | ||||
|             .map((e) => e.lastBackup ?? epoch) | ||||
|             .toList(), | ||||
|       ), | ||||
|     final selected = state.selectedBackupAlbums.map( | ||||
|       (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), | ||||
|     ); | ||||
|     final excluded = state.excludedBackupAlbums.map( | ||||
|       (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), | ||||
|     ); | ||||
|     final backupAlbums = selected.followedBy(excluded).toList(); | ||||
|     backupAlbums.sortBy((e) => e.id); | ||||
|     return _db.writeTxn(() async { | ||||
|       final dbAlbums = await _db.backupAlbums.where().sortById().findAll(); | ||||
|       final List<int> toDelete = []; | ||||
|       final List<BackupAlbum> toUpsert = []; | ||||
|       // stores the most recent `lastBackup` per album but always keeps the `selection` the user just made | ||||
|       diffSortedListsSync( | ||||
|         dbAlbums, | ||||
|         backupAlbums, | ||||
|         compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), | ||||
|         both: (BackupAlbum a, BackupAlbum b) { | ||||
|           b.lastBackup = | ||||
|               a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; | ||||
|           toUpsert.add(b); | ||||
|           return true; | ||||
|         }, | ||||
|         onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), | ||||
|         onlySecond: (BackupAlbum b) => toUpsert.add(b), | ||||
|       ); | ||||
|       await _db.backupAlbums.deleteAll(toDelete); | ||||
|       await _db.backupAlbums.putAll(toUpsert); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /// Invoke backup process | ||||
| @@ -447,7 +432,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|       Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets); | ||||
|       // Remove item that has already been backed up | ||||
|       for (var assetId in state.allAssetsInDatabase) { | ||||
|       for (final assetId in state.allAssetsInDatabase) { | ||||
|         assetsWillBeBackup.removeWhere((e) => e.id == assetId); | ||||
|       } | ||||
|  | ||||
| @@ -547,7 +532,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   } | ||||
|  | ||||
|   Future<void> _updateServerInfo() async { | ||||
|     var serverInfo = await _serverInfoService.getServerInfo(); | ||||
|     final serverInfo = await _serverInfoService.getServerInfo(); | ||||
|  | ||||
|     // Update server info | ||||
|     if (serverInfo != null) { | ||||
| @@ -559,7 +544,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|   Future<void> _resumeBackup() async { | ||||
|     // Check if user is login | ||||
|     var accessKey = Hive.box(userInfoBox).get(accessTokenKey); | ||||
|     final accessKey = Hive.box(userInfoBox).get(accessTokenKey); | ||||
|  | ||||
|     // User has been logged out return | ||||
|     if (accessKey == null || !_authState.isAuthenticated) { | ||||
| @@ -590,65 +575,56 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|   } | ||||
|  | ||||
|   Future<void> resumeBackup() async { | ||||
|     // 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); | ||||
|     final List<BackupAlbum> selectedBackupAlbums = await _db.backupAlbums | ||||
|         .filter() | ||||
|         .selectionEqualTo(BackupSelection.select) | ||||
|         .findAll(); | ||||
|     final List<BackupAlbum> excludedBackupAlbums = await _db.backupAlbums | ||||
|         .filter() | ||||
|         .selectionEqualTo(BackupSelection.select) | ||||
|         .findAll(); | ||||
|     Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums; | ||||
|     Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums; | ||||
|     if (albums != null) { | ||||
|     if (selectedAlbums.isNotEmpty) { | ||||
|       selectedAlbums = _updateAlbumsBackupTime( | ||||
|         selectedAlbums, | ||||
|           albums.selectedAlbumIds, | ||||
|           albums.lastSelectedBackupTime, | ||||
|         selectedBackupAlbums, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (excludedAlbums.isNotEmpty) { | ||||
|       excludedAlbums = _updateAlbumsBackupTime( | ||||
|         excludedAlbums, | ||||
|           albums.excludedAlbumsIds, | ||||
|           albums.lastExcludedBackupTime, | ||||
|         excludedBackupAlbums, | ||||
|       ); | ||||
|     } | ||||
|     } | ||||
|     final Box backgroundBox = Hive.box(backgroundBackupInfoBox); | ||||
|     final BackUpProgressEnum previous = state.backupProgress; | ||||
|     state = state.copyWith( | ||||
|       backupProgress: previous, | ||||
|       backupProgress: BackUpProgressEnum.inBackground, | ||||
|       selectedBackupAlbums: selectedAlbums, | ||||
|       excludedBackupAlbums: excludedAlbums, | ||||
|       backupRequireWifi: backgroundBox.get(backupRequireWifi), | ||||
|       backupRequireCharging: backgroundBox.get(backupRequireCharging), | ||||
|       backupTriggerDelay: backgroundBox.get(backupTriggerDelay), | ||||
|       backupRequireWifi: Store.get(StoreKey.backupRequireWifi), | ||||
|       backupRequireCharging: Store.get(StoreKey.backupRequireCharging), | ||||
|       backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay), | ||||
|     ); | ||||
|     // assumes the background service is currently running | ||||
|     // if true, waits until it has stopped to start the backup | ||||
|     final bool hasLock = await _backgroundService.acquireLock(); | ||||
|     if (hasLock) { | ||||
|       state = state.copyWith(backupProgress: previous); | ||||
|     } | ||||
|     return _resumeBackup(); | ||||
|   } | ||||
|  | ||||
|   Set<AvailableAlbum> _updateAlbumsBackupTime( | ||||
|     Set<AvailableAlbum> albums, | ||||
|     List<String> ids, | ||||
|     List<DateTime> times, | ||||
|     List<BackupAlbum> backupAlbums, | ||||
|   ) { | ||||
|     Set<AvailableAlbum> result = {}; | ||||
|     for (int i = 0; i < ids.length; i++) { | ||||
|     for (BackupAlbum ba in backupAlbums) { | ||||
|       try { | ||||
|         AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]); | ||||
|         result.add(a.copyWith(lastBackup: times[i])); | ||||
|         AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id); | ||||
|         result.add(a.copyWith(lastBackup: ba.lastBackup)); | ||||
|       } on StateError { | ||||
|         log.severe( | ||||
|           "[_updateAlbumBackupTime] failed to find album in state", | ||||
| @@ -667,35 +643,6 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|       AppStateEnum.detached, | ||||
|     ]; | ||||
|     if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { | ||||
|       try { | ||||
|         if (Hive.isBoxOpen(hiveBackupInfoBox)) { | ||||
|           await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close(); | ||||
|         } | ||||
|       } catch (error) { | ||||
|         log.info("[_notifyBackgroundServiceCanRun] failed to close box"); | ||||
|       } | ||||
|       try { | ||||
|         if (Hive.isBoxOpen(duplicatedAssetsBox)) { | ||||
|           await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close(); | ||||
|         } | ||||
|       } catch (error, stackTrace) { | ||||
|         log.severe( | ||||
|           "[_notifyBackgroundServiceCanRun] failed to close box", | ||||
|           error, | ||||
|           stackTrace, | ||||
|         ); | ||||
|       } | ||||
|       try { | ||||
|         if (Hive.isBoxOpen(backgroundBackupInfoBox)) { | ||||
|           await Hive.box(backgroundBackupInfoBox).close(); | ||||
|         } | ||||
|       } catch (error, stackTrace) { | ||||
|         log.severe( | ||||
|           "[_notifyBackgroundServiceCanRun] failed to close box", | ||||
|           error, | ||||
|           stackTrace, | ||||
|         ); | ||||
|       } | ||||
|       _backgroundService.releaseLock(); | ||||
|     } | ||||
|   } | ||||
| @@ -709,6 +656,7 @@ final backupProvider = | ||||
|     ref.watch(authenticationProvider), | ||||
|     ref.watch(backgroundServiceProvider), | ||||
|     ref.watch(galleryPermissionNotifier.notifier), | ||||
|     ref.watch(dbProvider), | ||||
|     ref, | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -8,31 +8,34 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| 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/shared/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| import 'package:immich_mobile/utils/files_helper.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| import 'package:http_parser/http_parser.dart'; | ||||
| import 'package:path/path.dart' as p; | ||||
| import 'package:cancellation_token_http/http.dart' as http; | ||||
|  | ||||
| import '../models/hive_duplicated_assets.model.dart'; | ||||
|  | ||||
| final backupServiceProvider = Provider( | ||||
|   (ref) => BackupService( | ||||
|     ref.watch(apiServiceProvider), | ||||
|     ref.watch(dbProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class BackupService { | ||||
|   final httpClient = http.Client(); | ||||
|   final ApiService _apiService; | ||||
|   final Isar _db; | ||||
|  | ||||
|   BackupService(this._apiService); | ||||
|   BackupService(this._apiService, this._db); | ||||
|  | ||||
|   Future<List<String>?> getDeviceBackupAsset() async { | ||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
| @@ -45,32 +48,28 @@ class BackupService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _saveDuplicatedAssetIdToLocalStorage(List<String> deviceAssetIds) { | ||||
|     HiveDuplicatedAssets duplicatedAssets = | ||||
|         Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) | ||||
|                 .get(duplicatedAssetsKey) ?? | ||||
|             HiveDuplicatedAssets(duplicatedAssetIds: []); | ||||
|  | ||||
|     duplicatedAssets.duplicatedAssetIds = | ||||
|         {...duplicatedAssets.duplicatedAssetIds, ...deviceAssetIds}.toList(); | ||||
|  | ||||
|     Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) | ||||
|         .put(duplicatedAssetsKey, duplicatedAssets); | ||||
|   Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) { | ||||
|     final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList(); | ||||
|     return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates)); | ||||
|   } | ||||
|  | ||||
|   /// Get duplicated asset id from Hive storage | ||||
|   Set<String> getDuplicatedAssetIds() { | ||||
|     HiveDuplicatedAssets duplicatedAssets = | ||||
|         Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) | ||||
|                 .get(duplicatedAssetsKey) ?? | ||||
|             HiveDuplicatedAssets(duplicatedAssetIds: []); | ||||
|  | ||||
|     return duplicatedAssets.duplicatedAssetIds.toSet(); | ||||
|   /// Get duplicated asset id from database | ||||
|   Future<Set<String>> getDuplicatedAssetIds() async { | ||||
|     final duplicates = await _db.duplicatedAssets.where().findAll(); | ||||
|     return duplicates.map((e) => e.id).toSet(); | ||||
|   } | ||||
|  | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       selectedAlbumsQuery() => | ||||
|           _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select); | ||||
|   QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> | ||||
|       excludedAlbumsQuery() => | ||||
|           _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); | ||||
|  | ||||
|   /// Returns all assets newer than the last successful backup per album | ||||
|   Future<List<AssetEntity>> buildUploadCandidates( | ||||
|     HiveBackupAlbums backupAlbums, | ||||
|     List<BackupAlbum> selectedBackupAlbums, | ||||
|     List<BackupAlbum> excludedBackupAlbums, | ||||
|   ) async { | ||||
|     final filter = FilterOptionGroup( | ||||
|       containsPathModified: true, | ||||
| @@ -81,66 +80,55 @@ class BackupService { | ||||
|     ); | ||||
|     final now = DateTime.now(); | ||||
|     final List<AssetPathEntity?> selectedAlbums = | ||||
|         await _loadAlbumsWithTimeFilter( | ||||
|       backupAlbums.selectedAlbumIds, | ||||
|       backupAlbums.lastSelectedBackupTime, | ||||
|       filter, | ||||
|       now, | ||||
|     ); | ||||
|         await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now); | ||||
|     if (selectedAlbums.every((e) => e == null)) { | ||||
|       return []; | ||||
|     } | ||||
|     final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll); | ||||
|     if (allIdx != -1) { | ||||
|       final List<AssetPathEntity?> excludedAlbums = | ||||
|           await _loadAlbumsWithTimeFilter( | ||||
|         backupAlbums.excludedAlbumsIds, | ||||
|         backupAlbums.lastExcludedBackupTime, | ||||
|         filter, | ||||
|         now, | ||||
|       ); | ||||
|           await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now); | ||||
|       final List<AssetEntity> toAdd = await _fetchAssetsAndUpdateLastBackup( | ||||
|         selectedAlbums.slice(allIdx, allIdx + 1), | ||||
|         backupAlbums.lastSelectedBackupTime.slice(allIdx, allIdx + 1), | ||||
|         selectedBackupAlbums.slice(allIdx, allIdx + 1), | ||||
|         now, | ||||
|       ); | ||||
|       final List<AssetEntity> toRemove = await _fetchAssetsAndUpdateLastBackup( | ||||
|         excludedAlbums, | ||||
|         backupAlbums.lastExcludedBackupTime, | ||||
|         excludedBackupAlbums, | ||||
|         now, | ||||
|       ); | ||||
|       return toAdd.toSet().difference(toRemove.toSet()).toList(); | ||||
|     } else { | ||||
|       return await _fetchAssetsAndUpdateLastBackup( | ||||
|         selectedAlbums, | ||||
|         backupAlbums.lastSelectedBackupTime, | ||||
|         selectedBackupAlbums, | ||||
|         now, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter( | ||||
|     List<String> albumIds, | ||||
|     List<DateTime> lastBackups, | ||||
|     List<BackupAlbum> albums, | ||||
|     FilterOptionGroup filter, | ||||
|     DateTime now, | ||||
|   ) async { | ||||
|     List<AssetPathEntity?> result = List.filled(albumIds.length, null); | ||||
|     for (int i = 0; i < albumIds.length; i++) { | ||||
|     List<AssetPathEntity?> result = []; | ||||
|     for (BackupAlbum a in albums) { | ||||
|       try { | ||||
|         final AssetPathEntity album = | ||||
|             await AssetPathEntity.obtainPathFromProperties( | ||||
|           id: albumIds[i], | ||||
|           id: a.id, | ||||
|           optionGroup: filter.copyWith( | ||||
|             updateTimeCond: DateTimeCond( | ||||
|               // subtract 2 seconds to prevent missing assets due to rounding issues | ||||
|               min: lastBackups[i].subtract(const Duration(seconds: 2)), | ||||
|               min: a.lastBackup.subtract(const Duration(seconds: 2)), | ||||
|               max: now, | ||||
|             ), | ||||
|           ), | ||||
|           maxDateTimeToNow: false, | ||||
|         ); | ||||
|         result[i] = album; | ||||
|         result.add(album); | ||||
|       } on StateError { | ||||
|         // either there are no assets matching the filter criteria OR the album no longer exists | ||||
|       } | ||||
| @@ -150,17 +138,18 @@ class BackupService { | ||||
|  | ||||
|   Future<List<AssetEntity>> _fetchAssetsAndUpdateLastBackup( | ||||
|     List<AssetPathEntity?> albums, | ||||
|     List<DateTime> lastBackup, | ||||
|     List<BackupAlbum> backupAlbums, | ||||
|     DateTime now, | ||||
|   ) async { | ||||
|     List<AssetEntity> result = []; | ||||
|     for (int i = 0; i < albums.length; i++) { | ||||
|       final AssetPathEntity? a = albums[i]; | ||||
|       if (a != null && a.lastModified?.isBefore(lastBackup[i]) != true) { | ||||
|       if (a != null && | ||||
|           a.lastModified?.isBefore(backupAlbums[i].lastBackup) != true) { | ||||
|         result.addAll( | ||||
|           await a.getAssetListRange(start: 0, end: await a.assetCountAsync), | ||||
|         ); | ||||
|         lastBackup[i] = now; | ||||
|         backupAlbums[i].lastBackup = now; | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
| @@ -173,7 +162,7 @@ class BackupService { | ||||
|     if (candidates.isEmpty) { | ||||
|       return candidates; | ||||
|     } | ||||
|     final Set<String> duplicatedAssetIds = getDuplicatedAssetIds(); | ||||
|     final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds(); | ||||
|     candidates = duplicatedAssetIds.isEmpty | ||||
|         ? candidates | ||||
|         : candidates | ||||
| @@ -261,7 +250,8 @@ class BackupService { | ||||
|           req.fields['deviceId'] = deviceId; | ||||
|           req.fields['assetType'] = _getAssetType(entity.type); | ||||
|           req.fields['fileCreatedAt'] = entity.createDateTime.toIso8601String(); | ||||
|           req.fields['fileModifiedAt'] = entity.modifiedDateTime.toIso8601String(); | ||||
|           req.fields['fileModifiedAt'] = | ||||
|               entity.modifiedDateTime.toIso8601String(); | ||||
|           req.fields['isFavorite'] = entity.isFavorite.toString(); | ||||
|           req.fields['fileExtension'] = fileExtension; | ||||
|           req.fields['duration'] = entity.videoDuration.toString(); | ||||
| @@ -332,7 +322,7 @@ class BackupService { | ||||
|       } | ||||
|     } | ||||
|     if (duplicatedAssetIds.isNotEmpty) { | ||||
|       _saveDuplicatedAssetIdToLocalStorage(duplicatedAssetIds); | ||||
|       await _saveDuplicatedAssetIds(duplicatedAssetIds); | ||||
|     } | ||||
|     return !anyErrors; | ||||
|   } | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|     AuthenticationState authenticationState = ref.watch(authenticationProvider); | ||||
|     final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings; | ||||
|  | ||||
|     final appRefreshDisabled = Platform.isIOS && | ||||
|       settings?.appRefreshEnabled != true; | ||||
|     final appRefreshDisabled = | ||||
|         Platform.isIOS && settings?.appRefreshEnabled != true; | ||||
|     bool hasExclusiveAccess = | ||||
|         backupState.backupProgress != BackUpProgressEnum.inBackground; | ||||
|     bool shouldBackup = backupState.allUniqueAssets.length - | ||||
| @@ -292,15 +292,13 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                     dense: true, | ||||
|                     activeColor: activeColor, | ||||
|                     value: isWifiRequired, | ||||
|                     onChanged: hasExclusiveAccess | ||||
|                         ? (isChecked) => ref | ||||
|                     onChanged: (isChecked) => ref | ||||
|                         .read(backupProvider.notifier) | ||||
|                         .configureBackgroundBackup( | ||||
|                           requireWifi: isChecked, | ||||
|                           onError: showErrorToUser, | ||||
|                           onBatteryInfo: showBatteryOptimizationInfoToUser, | ||||
|                             ) | ||||
|                         : null, | ||||
|                         ), | ||||
|                   ), | ||||
|                 if (isBackgroundEnabled) | ||||
|                   SwitchListTile.adaptive( | ||||
| @@ -314,21 +312,18 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                     dense: true, | ||||
|                     activeColor: activeColor, | ||||
|                     value: isChargingRequired, | ||||
|                     onChanged: hasExclusiveAccess | ||||
|                         ? (isChecked) => ref | ||||
|                     onChanged: (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( | ||||
| @@ -339,9 +334,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                     ), | ||||
|                     subtitle: Slider( | ||||
|                       value: triggerDelay.value, | ||||
|                       onChanged: hasExclusiveAccess | ||||
|                           ? (double v) => triggerDelay.value = v | ||||
|                           : null, | ||||
|                       onChanged: (double v) => triggerDelay.value = v, | ||||
|                       onChangeEnd: (double v) => ref | ||||
|                           .read(backupProvider.notifier) | ||||
|                           .configureBackgroundBackup( | ||||
| @@ -385,9 +378,7 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                 final enabled = snapshot.data as bool?; | ||||
|                 // If it's not enabled, show them some kind of alert that says | ||||
|                 // background refresh is not enabled | ||||
|                 if (enabled != null && !enabled) { | ||||
|  | ||||
|                 } | ||||
|                 if (enabled != null && !enabled) {} | ||||
|                 // If it's enabled, no need to bother them | ||||
|                 return Container(); | ||||
|               }, | ||||
| @@ -403,7 +394,9 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|     Widget buildBackgroundAppRefreshWarning() { | ||||
|       return ListTile( | ||||
|         isThreeLine: true, | ||||
|         leading: const Icon(Icons.task_outlined,), | ||||
|         leading: const Icon( | ||||
|           Icons.task_outlined, | ||||
|         ), | ||||
|         title: const Text( | ||||
|           'backup_controller_page_background_app_refresh_disabled_title', | ||||
|           style: TextStyle( | ||||
| @@ -533,12 +526,9 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             ), | ||||
|           ), | ||||
|           trailing: ElevatedButton( | ||||
|             onPressed: hasExclusiveAccess | ||||
|                 ? () { | ||||
|                     AutoRouter.of(context) | ||||
|                         .push(const BackupAlbumSelectionRoute()); | ||||
|                   } | ||||
|                 : null, | ||||
|             onPressed: () { | ||||
|               AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); | ||||
|             }, | ||||
|             child: const Text( | ||||
|               "backup_controller_page_select", | ||||
|               style: TextStyle( | ||||
| @@ -598,26 +588,10 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     buildBackgroundBackupInfo() { | ||||
|       return hasExclusiveAccess | ||||
|           ? const SizedBox.shrink() | ||||
|           : Card( | ||||
|               shape: RoundedRectangleBorder( | ||||
|                 borderRadius: BorderRadius.circular(20), // if you need this | ||||
|                 side: BorderSide( | ||||
|                   color: isDarkMode | ||||
|                       ? const Color.fromARGB(255, 56, 56, 56) | ||||
|                       : Colors.black12, | ||||
|                   width: 1, | ||||
|                 ), | ||||
|               ), | ||||
|               elevation: 0, | ||||
|               borderOnForeground: false, | ||||
|               child: const Padding( | ||||
|                 padding: EdgeInsets.all(16.0), | ||||
|                 child: Text( | ||||
|                   "Background backup is currently running, some actions are disabled", | ||||
|                   style: TextStyle(fontWeight: FontWeight.bold), | ||||
|                 ), | ||||
|       return const ListTile( | ||||
|         leading: Icon(Icons.info_outline_rounded), | ||||
|         title: Text( | ||||
|           "Background backup is currently running, cannot start manual backup", | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
| @@ -652,7 +626,6 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|                 style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
|               ).tr(), | ||||
|             ), | ||||
|             buildBackgroundBackupInfo(), | ||||
|             buildFolderSelectionTile(), | ||||
|             BackupInfoCard( | ||||
|               title: "backup_controller_page_total".tr(), | ||||
| @@ -681,22 +654,20 @@ class BackupControllerPage extends HookConsumerWidget { | ||||
|             AnimatedSwitcher( | ||||
|               duration: const Duration(milliseconds: 500), | ||||
|               child: Platform.isIOS | ||||
|               ? ( | ||||
|                 appRefreshDisabled | ||||
|                   ? (appRefreshDisabled | ||||
|                       ? buildBackgroundAppRefreshWarning() | ||||
|                   : buildBackgroundBackupController() | ||||
|               ) : buildBackgroundBackupController(), | ||||
|                       : buildBackgroundBackupController()) | ||||
|                   : buildBackgroundBackupController(), | ||||
|             ), | ||||
|             const Divider(), | ||||
|             buildStorageInformation(), | ||||
|             const Divider(), | ||||
|             const CurrentUploadingAssetInfoBox(), | ||||
|             if (!hasExclusiveAccess) buildBackgroundBackupInfo(), | ||||
|             buildBackupButton() | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'dart:convert'; | ||||
| @@ -9,7 +10,8 @@ part 'store.g.dart'; | ||||
| /// Can be used concurrently from multiple isolates | ||||
| class Store { | ||||
|   static late final Isar _db; | ||||
|   static final List<dynamic> _cache = List.filled(StoreKey.values.length, null); | ||||
|   static final List<dynamic> _cache = | ||||
|       List.filled(StoreKey.values.map((e) => e.id).max + 1, null); | ||||
|  | ||||
|   /// Initializes the store (call exactly once per app start) | ||||
|   static void init(Isar db) { | ||||
| @@ -70,23 +72,44 @@ class StoreValue { | ||||
|   int? intValue; | ||||
|   String? strValue; | ||||
|  | ||||
|   T? _extract<T>(StoreKey key) => key.isInt | ||||
|       ? (key.fromDb == null ? intValue : key.fromDb!.call(Store._db, intValue!)) | ||||
|       : (key.fromJson != null | ||||
|           ? key.fromJson!(json.decode(strValue!)) | ||||
|           : strValue); | ||||
|   static Future<StoreValue> _of(dynamic value, StoreKey key) async => | ||||
|       StoreValue( | ||||
|         key.id, | ||||
|         intValue: key.isInt | ||||
|             ? (key.toDb == null | ||||
|                 ? value | ||||
|                 : await key.toDb!.call(Store._db, value)) | ||||
|             : null, | ||||
|         strValue: key.isInt | ||||
|   dynamic _extract(StoreKey key) { | ||||
|     switch (key.type) { | ||||
|       case int: | ||||
|         return key.fromDb == null | ||||
|             ? intValue | ||||
|             : key.fromDb!.call(Store._db, intValue!); | ||||
|       case bool: | ||||
|         return intValue == null ? null : intValue! == 1; | ||||
|       case DateTime: | ||||
|         return intValue == null | ||||
|             ? null | ||||
|             : (key.fromJson == null ? value : json.encode(value.toJson())), | ||||
|       ); | ||||
|             : DateTime.fromMicrosecondsSinceEpoch(intValue!); | ||||
|       case String: | ||||
|         return key.fromJson != null | ||||
|             ? key.fromJson!.call(json.decode(strValue!)) | ||||
|             : strValue; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static Future<StoreValue> _of(dynamic value, StoreKey key) async { | ||||
|     int? i; | ||||
|     String? s; | ||||
|     switch (key.type) { | ||||
|       case int: | ||||
|         i = (key.toDb == null ? value : await key.toDb!.call(Store._db, value)); | ||||
|         break; | ||||
|       case bool: | ||||
|         i = value == null ? null : (value ? 1 : 0); | ||||
|         break; | ||||
|       case DateTime: | ||||
|         i = value == null ? null : (value as DateTime).microsecondsSinceEpoch; | ||||
|         break; | ||||
|       case String: | ||||
|         s = key.fromJson == null ? value : json.encode(value.toJson()); | ||||
|         break; | ||||
|     } | ||||
|     return StoreValue(key.id, intValue: i, strValue: s); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Key for each possible value in the `Store`. | ||||
| @@ -94,21 +117,24 @@ class StoreValue { | ||||
| enum StoreKey { | ||||
|   userRemoteId(0), | ||||
|   assetETag(1), | ||||
|   currentUser(2, isInt: true, fromDb: _getUser, toDb: _toUser), | ||||
|   deviceIdHash(3, isInt: true), | ||||
|   currentUser(2, type: int, fromDb: _getUser, toDb: _toUser), | ||||
|   deviceIdHash(3, type: int), | ||||
|   deviceId(4), | ||||
|   ; | ||||
|   backupFailedSince(5, type: DateTime), | ||||
|   backupRequireWifi(6, type: bool), | ||||
|   backupRequireCharging(7, type: bool), | ||||
|   backupTriggerDelay(8, type: int); | ||||
|  | ||||
|   const StoreKey( | ||||
|     this.id, { | ||||
|     this.isInt = false, | ||||
|     this.type = String, | ||||
|     this.fromDb, | ||||
|     this.toDb, | ||||
|     // ignore: unused_element | ||||
|     this.fromJson, | ||||
|   }); | ||||
|   final int id; | ||||
|   final bool isInt; | ||||
|   final Type type; | ||||
|   final dynamic Function(Isar, int)? fromDb; | ||||
|   final Future<int> Function(Isar, dynamic)? toDb; | ||||
|   final Function(dynamic)? fromJson; | ||||
|   | ||||
| @@ -4,22 +4,102 @@ import 'package:flutter/cupertino.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album_cache.service.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/services/asset_cache.service.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
|  | ||||
| Future<void> migrateHiveToStoreIfNecessary() async { | ||||
|   await _migrateHiveBoxIfNecessary(userInfoBox, _migrateHiveUserInfoBox); | ||||
|   await _migrateHiveBoxIfNecessary( | ||||
|     backgroundBackupInfoBox, | ||||
|     _migrateHiveBackgroundBackupInfoBox, | ||||
|   ); | ||||
|   await _migrateHiveBoxIfNecessary(hiveBackupInfoBox, _migrateBackupInfoBox); | ||||
|   await _migrateHiveBoxIfNecessary( | ||||
|     duplicatedAssetsBox, | ||||
|     _migrateDuplicatedAssetsBox, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateHiveUserInfoBox(Box box) async { | ||||
|   await _migrateKey(box, userIdKey, StoreKey.userRemoteId); | ||||
|   await _migrateKey(box, assetEtagKey, StoreKey.assetETag); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async { | ||||
|   await _migrateKey(box, backupFailedSince, StoreKey.backupFailedSince); | ||||
|   await _migrateKey(box, backupRequireWifi, StoreKey.backupRequireWifi); | ||||
|   await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging); | ||||
|   await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay); | ||||
|   return box.deleteFromDisk(); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async { | ||||
|   final Isar? db = Isar.getInstance(); | ||||
|   if (db == null) { | ||||
|     throw Exception("_migrateBackupInfoBox could not load database"); | ||||
|   } | ||||
|   final HiveBackupAlbums? infos = box.get(backupInfoKey); | ||||
|   if (infos != null) { | ||||
|     List<BackupAlbum> albums = []; | ||||
|     for (int i = 0; i < infos.selectedAlbumIds.length; i++) { | ||||
|       final album = BackupAlbum( | ||||
|         infos.selectedAlbumIds[i], | ||||
|         infos.lastSelectedBackupTime[i], | ||||
|         BackupSelection.select, | ||||
|       ); | ||||
|       albums.add(album); | ||||
|     } | ||||
|     for (int i = 0; i < infos.excludedAlbumsIds.length; i++) { | ||||
|       final album = BackupAlbum( | ||||
|         infos.excludedAlbumsIds[i], | ||||
|         infos.lastExcludedBackupTime[i], | ||||
|         BackupSelection.exclude, | ||||
|       ); | ||||
|       albums.add(album); | ||||
|     } | ||||
|     await db.writeTxn(() => db.backupAlbums.putAll(albums)); | ||||
|   } else { | ||||
|     debugPrint("_migrateBackupInfoBox deletes empty box"); | ||||
|   } | ||||
|   return box.deleteFromDisk(); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) async { | ||||
|   final Isar? db = Isar.getInstance(); | ||||
|   if (db == null) { | ||||
|     throw Exception("_migrateBackupInfoBox could not load database"); | ||||
|   } | ||||
|   final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey); | ||||
|   if (duplicatedAssets != null) { | ||||
|     final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds | ||||
|         .map((id) => DuplicatedAsset(id)) | ||||
|         .toList(); | ||||
|     await db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds)); | ||||
|   } else { | ||||
|     debugPrint("_migrateDuplicatedAssetsBox deletes empty box"); | ||||
|   } | ||||
|   return box.deleteFromDisk(); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateHiveBoxIfNecessary<T>( | ||||
|   String boxName, | ||||
|   Future<void> Function(Box<T>) migrate, | ||||
| ) async { | ||||
|   try { | ||||
|     if (await Hive.boxExists(userInfoBox)) { | ||||
|       final Box box = await Hive.openBox(userInfoBox); | ||||
|       await _migrateSingleKey(box, userIdKey, StoreKey.userRemoteId); | ||||
|       await _migrateSingleKey(box, assetEtagKey, StoreKey.assetETag); | ||||
|     if (await Hive.boxExists(boxName)) { | ||||
|       await migrate(await Hive.openBox<T>(boxName)); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     debugPrint("Error while migrating userInfoBox $e"); | ||||
|     debugPrint("Error while migrating $boxName $e"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| _migrateSingleKey(Box box, String hiveKey, StoreKey key) async { | ||||
| _migrateKey(Box box, String hiveKey, StoreKey key) async { | ||||
|   final String? value = box.get(hiveKey); | ||||
|   if (value != null) { | ||||
|     await Store.put(key, value); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user