1
0
forked from Cutlery/immich

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:
Fynn Petersen-Frey 2023-03-18 15:55:11 +01:00 committed by GitHub
parent f56eaae019
commit 05cf5d57a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1533 additions and 379 deletions

View File

@ -9,6 +9,8 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.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/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_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
@ -104,6 +106,8 @@ Future<Isar> loadDb() async {
AssetSchema, AssetSchema,
AlbumSchema, AlbumSchema,
UserSchema, UserSchema,
BackupAlbumSchema,
DuplicatedAssetSchema,
], ],
directory: dir.path, directory: dir.path,
maxSizeMiB: 256, maxSizeMiB: 256,
@ -156,10 +160,12 @@ class ImmichAppState extends ConsumerState<ImmichApp>
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
ref.watch(notificationPermissionProvider.notifier) ref
.getNotificationPermission(); .watch(notificationPermissionProvider.notifier)
ref.watch(galleryPermissionNotifier.notifier) .getNotificationPermission();
.getGalleryPermissionStatus(); ref
.watch(galleryPermissionNotifier.notifier)
.getGalleryPermissionStatus();
ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); ref.read(iOSBackgroundSettingsProvider.notifier).refresh();

View File

@ -2,11 +2,9 @@ import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/background_service/background.service.dart'; import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
@ -24,27 +22,27 @@ final albumServiceProvider = Provider(
(ref) => AlbumService( (ref) => AlbumService(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(userServiceProvider), ref.watch(userServiceProvider),
ref.watch(backgroundServiceProvider),
ref.watch(syncServiceProvider), ref.watch(syncServiceProvider),
ref.watch(dbProvider), ref.watch(dbProvider),
ref.watch(backupServiceProvider),
), ),
); );
class AlbumService { class AlbumService {
final ApiService _apiService; final ApiService _apiService;
final UserService _userService; final UserService _userService;
final BackgroundService _backgroundService;
final SyncService _syncService; final SyncService _syncService;
final Isar _db; final Isar _db;
final BackupService _backupService;
Completer<bool> _localCompleter = Completer()..complete(false); Completer<bool> _localCompleter = Completer()..complete(false);
Completer<bool> _remoteCompleter = Completer()..complete(false); Completer<bool> _remoteCompleter = Completer()..complete(false);
AlbumService( AlbumService(
this._apiService, this._apiService,
this._userService, this._userService,
this._backgroundService,
this._syncService, this._syncService,
this._db, this._db,
this._backupService,
); );
/// Checks all selected device albums for changes of albums and their assets /// Checks all selected device albums for changes of albums and their assets
@ -58,13 +56,11 @@ class AlbumService {
final Stopwatch sw = Stopwatch()..start(); final Stopwatch sw = Stopwatch()..start();
bool changes = false; bool changes = false;
try { try {
if (!await _backgroundService.hasAccess) { final List<String> excludedIds =
return false; await _backupService.excludedAlbumsQuery().idProperty().findAll();
} final List<String> selectedIds =
final HiveBackupAlbums? infos = await _backupService.selectedAlbumsQuery().idProperty().findAll();
(await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox)) if (selectedIds.isEmpty) {
.get(backupInfoKey);
if (infos == null) {
return false; return false;
} }
final List<AssetPathEntity> onDevice = final List<AssetPathEntity> onDevice =
@ -72,11 +68,11 @@ class AlbumService {
hasAll: true, hasAll: true,
filterOption: FilterOptionGroup(containsPathModified: true), filterOption: FilterOptionGroup(containsPathModified: true),
); );
if (infos.excludedAlbumsIds.isNotEmpty) { if (excludedIds.isNotEmpty) {
// remove all excluded albums // 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)) .map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
.whereNotNull() .whereNotNull()
.any((a) => a.isAll); .any((a) => a.isAll);
@ -85,7 +81,7 @@ class AlbumService {
onDevice.removeWhere((e) => e.isAll); onDevice.removeWhere((e) => e.isAll);
} else { } else {
// keep only the explicitly selected albums // 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); changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice);
} finally { } finally {

View File

@ -4,21 +4,25 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities; import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.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/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/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_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/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.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/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/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:path_provider_ios/path_provider_ios.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -51,10 +55,6 @@ class BackgroundService {
_Throttle(_updateProgress, notifyInterval); _Throttle(_updateProgress, notifyInterval);
late final _Throttle _throttledDetailNotify = late final _Throttle _throttledDetailNotify =
_Throttle(_updateDetailProgress, notifyInterval); _Throttle(_updateDetailProgress, notifyInterval);
Completer<bool> _hasAccessCompleter = Completer();
late Future<bool> _hasAccess = _hasAccessCompleter.future;
Future<bool> get hasAccess => _hasAccess;
bool get isBackgroundInitialized { bool get isBackgroundInitialized {
return _isBackgroundInitialized; return _isBackgroundInitialized;
@ -194,11 +194,6 @@ class BackgroundService {
debugPrint("WARNING: [acquireLock] called more than once"); debugPrint("WARNING: [acquireLock] called more than once");
return true; return true;
} }
if (_hasAccessCompleter.isCompleted) {
debugPrint("WARNING: [acquireLock] _hasAccessCompleter is completed");
_hasAccessCompleter = Completer();
_hasAccess = _hasAccessCompleter.future;
}
final int lockTime = Timeline.now; final int lockTime = Timeline.now;
_wantsLockTime = lockTime; _wantsLockTime = lockTime;
final ReceivePort rp = ReceivePort(_portNameLock); final ReceivePort rp = ReceivePort(_portNameLock);
@ -217,7 +212,6 @@ class BackgroundService {
} }
_hasLock = true; _hasLock = true;
rp.listen(_heartbeatListener); rp.listen(_heartbeatListener);
_hasAccessCompleter.complete(true);
return true; return true;
} }
@ -267,8 +261,6 @@ class BackgroundService {
void releaseLock() { void releaseLock() {
_wantsLockTime = 0; _wantsLockTime = 0;
if (_hasLock) { if (_hasLock) {
_hasAccessCompleter = Completer();
_hasAccess = _hasAccessCompleter.future;
IsolateNameServer.removePortNameMapping(_portNameLock); IsolateNameServer.removePortNameMapping(_portNameLock);
_waitingIsolate?.send(true); _waitingIsolate?.send(true);
_waitingIsolate = null; _waitingIsolate = null;
@ -339,29 +331,24 @@ class BackgroundService {
} }
Future<bool> _onAssetsChanged() async { Future<bool> _onAssetsChanged() async {
final Isar db = await loadDb();
await Hive.initFlutter(); await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter()); Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
await Future.wait([ await Future.wait([
Hive.openBox(userInfoBox), Hive.openBox(userInfoBox),
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox), Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
Hive.openBox(userSettingInfoBox), Hive.openBox(userSettingInfoBox),
Hive.openBox(backgroundBackupInfoBox),
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
]); ]);
ApiService apiService = ApiService(); ApiService apiService = ApiService();
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey)); apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
BackupService backupService = BackupService(apiService); BackupService backupService = BackupService(apiService, db);
AppSettingsService settingsService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService();
final Box<HiveBackupAlbums> box = final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox); final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey); if (selectedAlbums.isEmpty) {
if (backupAlbumInfo == null) {
return true; return true;
} }
@ -371,18 +358,37 @@ class BackgroundService {
final bool backupOk = await _runBackup( final bool backupOk = await _runBackup(
backupService, backupService,
settingsService, settingsService,
backupAlbumInfo, selectedAlbums,
excludedAlbums,
); );
if (backupOk) { if (backupOk) {
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince); await Store.delete(StoreKey.backupFailedSince);
await box.put( final backupAlbums = [...selectedAlbums, ...excludedAlbums];
backupInfoKey, backupAlbums.sortBy((e) => e.id);
backupAlbumInfo, db.writeTxnSync(() {
); final dbAlbums = db.backupAlbums.where().sortById().findAllSync();
} else if (Hive.box(backgroundBackupInfoBox).get(backupFailedSince) == final List<int> toDelete = [];
null) { final List<BackupAlbum> toUpsert = [];
Hive.box(backgroundBackupInfoBox) // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
.put(backupFailedSince, DateTime.now()); 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),
);
db.backupAlbums.deleteAllSync(toDelete);
db.backupAlbums.putAllSync(toUpsert);
});
} else if (Store.get(StoreKey.backupFailedSince) == null) {
Store.put(StoreKey.backupFailedSince, DateTime.now());
return false; return false;
} }
// Android should check for new assets added while performing backup // Android should check for new assets added while performing backup
@ -395,7 +401,8 @@ class BackgroundService {
Future<bool> _runBackup( Future<bool> _runBackup(
BackupService backupService, BackupService backupService,
AppSettingsService settingsService, AppSettingsService settingsService,
HiveBackupAlbums backupAlbumInfo, List<BackupAlbum> selectedAlbums,
List<BackupAlbum> excludedAlbums,
) async { ) async {
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
final bool notifyTotalProgress = settingsService final bool notifyTotalProgress = settingsService
@ -407,8 +414,10 @@ class BackgroundService {
return false; return false;
} }
List<AssetEntity> toUpload = List<AssetEntity> toUpload = await backupService.buildUploadCandidates(
await backupService.buildUploadCandidates(backupAlbumInfo); selectedAlbums,
excludedAlbums,
);
try { try {
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
@ -520,8 +529,7 @@ class BackgroundService {
} else if (value == 5) { } else if (value == 5) {
return false; return false;
} }
final DateTime? failedSince = final DateTime? failedSince = Store.get(StoreKey.backupFailedSince);
Hive.box(backgroundBackupInfoBox).get(backupFailedSince);
if (failedSince == null) { if (failedSince == null) {
return false; return false;
} }

View 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;
}

View 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');
});
}
}

View 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);
}

View 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');
});
}
}

View File

@ -1,22 +1,26 @@
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.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/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/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.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/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/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.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/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.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/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.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/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/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:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -29,6 +33,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
this._authState, this._authState,
this._backgroundService, this._backgroundService,
this._galleryPermissionNotifier, this._galleryPermissionNotifier,
this._db,
this.ref, this.ref,
) : super( ) : super(
BackUpState( BackUpState(
@ -69,6 +74,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final AuthenticationState _authState; final AuthenticationState _authState;
final BackgroundService _backgroundService; final BackgroundService _backgroundService;
final GalleryPermissionNotifier _galleryPermissionNotifier; final GalleryPermissionNotifier _galleryPermissionNotifier;
final Isar _db;
final Ref ref; final Ref ref;
/// ///
@ -157,11 +163,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
triggerMaxDelay: state.backupTriggerDelay * 10, triggerMaxDelay: state.backupTriggerDelay * 10,
); );
if (success) { if (success) {
final box = Hive.box(backgroundBackupInfoBox);
await Future.wait([ await Future.wait([
box.put(backupRequireWifi, state.backupRequireWifi), Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi),
box.put(backupRequireCharging, state.backupRequireCharging), Store.put(
box.put(backupTriggerDelay, state.backupTriggerDelay), StoreKey.backupRequireCharging,
state.backupRequireCharging,
),
Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay),
]); ]);
} else { } else {
state = state.copyWith( state = state.copyWith(
@ -201,16 +209,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
for (AssetPathEntity album in albums) { for (AssetPathEntity album in albums) {
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
var assetCountInAlbum = await album.assetCountAsync; final assetCountInAlbum = await album.assetCountAsync;
if (assetCountInAlbum > 0) { if (assetCountInAlbum > 0) {
var assetList = final assetList =
await album.getAssetListRange(start: 0, end: assetCountInAlbum); await album.getAssetListRange(start: 0, end: assetCountInAlbum);
if (assetList.isNotEmpty) { if (assetList.isNotEmpty) {
var thumbnailAsset = assetList.first; final thumbnailAsset = assetList.first;
try { try {
var thumbnailData = await thumbnailAsset final thumbnailData = await thumbnailAsset
.thumbnailDataWithSize(const ThumbnailSize(512, 512)); .thumbnailDataWithSize(const ThumbnailSize(512, 512));
availableAlbum = availableAlbum =
availableAlbum.copyWith(thumbnailData: thumbnailData); availableAlbum.copyWith(thumbnailData: thumbnailData);
@ -229,34 +237,17 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(availableAlbums: availableAlbums); state = state.copyWith(availableAlbums: availableAlbums);
// Put persistent storage info into local state of the app final List<BackupAlbum> excludedBackupAlbums =
// Get local storage on selected backup album await _backupService.excludedAlbumsQuery().findAll();
Box<HiveBackupAlbums> backupAlbumInfoBox = final List<BackupAlbum> selectedBackupAlbums =
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox); await _backupService.selectedAlbumsQuery().findAll();
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;
}
// First time backup - set isAll album is the default one for backup. // 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"); log.info("First time backup; setup 'Recent(s)' album as default");
// Get album that contains all assets // Get album that contains all assets
var list = await PhotoManager.getAssetPathList( final list = await PhotoManager.getAssetPathList(
hasAll: true, hasAll: true,
onlyAll: true, onlyAll: true,
type: RequestType.common, type: RequestType.common,
@ -267,48 +258,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} }
AssetPathEntity albumHasAllAssets = list.first; AssetPathEntity albumHasAllAssets = list.first;
backupAlbumInfoBox.put( final ba = BackupAlbum(
backupInfoKey, albumHasAllAssets.id,
HiveBackupAlbums( DateTime.fromMillisecondsSinceEpoch(0),
selectedAlbumIds: [albumHasAllAssets.id], BackupSelection.select,
excludedAlbumsIds: [],
lastSelectedBackupTime: [
DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)
],
lastExcludedBackupTime: [],
),
); );
await _db.writeTxn(() => _db.backupAlbums.put(ba));
backupAlbumInfo = backupAlbumInfoBox.get(backupInfoKey);
} }
// Generate AssetPathEntity from id to add to local state // Generate AssetPathEntity from id to add to local state
try { try {
Set<AvailableAlbum> selectedAlbums = {}; final Set<AvailableAlbum> selectedAlbums = {};
for (var i = 0; i < backupAlbumInfo!.selectedAlbumIds.length; i++) { for (final BackupAlbum ba in selectedBackupAlbums) {
var albumAsset = final albumAsset = await AssetPathEntity.fromId(ba.id);
await AssetPathEntity.fromId(backupAlbumInfo.selectedAlbumIds[i]);
selectedAlbums.add( selectedAlbums.add(
AvailableAlbum( AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
albumEntity: albumAsset,
lastBackup: backupAlbumInfo.lastSelectedBackupTime.length > i
? backupAlbumInfo.lastSelectedBackupTime[i]
: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
),
); );
} }
Set<AvailableAlbum> excludedAlbums = {}; final Set<AvailableAlbum> excludedAlbums = {};
for (var i = 0; i < backupAlbumInfo.excludedAlbumsIds.length; i++) { for (final BackupAlbum ba in excludedBackupAlbums) {
var albumAsset = final albumAsset = await AssetPathEntity.fromId(ba.id);
await AssetPathEntity.fromId(backupAlbumInfo.excludedAlbumsIds[i]);
excludedAlbums.add( excludedAlbums.add(
AvailableAlbum( AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
albumEntity: albumAsset,
lastBackup: backupAlbumInfo.lastExcludedBackupTime.length > i
? backupAlbumInfo.lastExcludedBackupTime[i]
: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
),
); );
} }
state = state.copyWith( state = state.copyWith(
@ -328,36 +300,36 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Those assets are unique and are used as the total assets /// Those assets are unique and are used as the total assets
/// ///
Future<void> _updateBackupAssetCount() async { Future<void> _updateBackupAssetCount() async {
Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds(); final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
Set<AssetEntity> assetsFromSelectedAlbums = {}; final Set<AssetEntity> assetsFromSelectedAlbums = {};
Set<AssetEntity> assetsFromExcludedAlbums = {}; final Set<AssetEntity> assetsFromExcludedAlbums = {};
for (var album in state.selectedBackupAlbums) { for (final album in state.selectedBackupAlbums) {
var assets = await album.albumEntity.getAssetListRange( final assets = await album.albumEntity.getAssetListRange(
start: 0, start: 0,
end: await album.albumEntity.assetCountAsync, end: await album.albumEntity.assetCountAsync,
); );
assetsFromSelectedAlbums.addAll(assets); assetsFromSelectedAlbums.addAll(assets);
} }
for (var album in state.excludedBackupAlbums) { for (final album in state.excludedBackupAlbums) {
var assets = await album.albumEntity.getAssetListRange( final assets = await album.albumEntity.getAssetListRange(
start: 0, start: 0,
end: await album.albumEntity.assetCountAsync, end: await album.albumEntity.assetCountAsync,
); );
assetsFromExcludedAlbums.addAll(assets); assetsFromExcludedAlbums.addAll(assets);
} }
Set<AssetEntity> allUniqueAssets = final Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
var allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
if (allAssetsInDatabase == null) { if (allAssetsInDatabase == null) {
return; return;
} }
// Find asset that were backup from selected albums // Find asset that were backup from selected albums
Set<String> selectedAlbumsBackupAssets = final Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.id)); Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets selectedAlbumsBackupAssets
@ -386,7 +358,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} }
// Save to persistent storage // Save to persistent storage
_updatePersistentAlbumsSelection(); await _updatePersistentAlbumsSelection();
return; return;
} }
@ -395,7 +367,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// which albums are selected or excluded /// which albums are selected or excluded
/// and then update the UI according to those information /// and then update the UI according to those information
Future<void> getBackupInfo() async { Future<void> getBackupInfo() async {
var isEnabled = await _backgroundService.isBackgroundBackupEnabled(); final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
state = state.copyWith(backgroundBackup: isEnabled); state = state.copyWith(backgroundBackup: isEnabled);
@ -406,25 +378,38 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} }
} }
/// Save user selection of selected albums and excluded albums to /// Save user selection of selected albums and excluded albums to database
/// Hive database Future<void> _updatePersistentAlbumsSelection() {
void _updatePersistentAlbumsSelection() {
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
Box<HiveBackupAlbums> backupAlbumInfoBox = final selected = state.selectedBackupAlbums.map(
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox); (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
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 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 /// Invoke backup process
@ -447,7 +432,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets); Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up // 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); assetsWillBeBackup.removeWhere((e) => e.id == assetId);
} }
@ -547,7 +532,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} }
Future<void> _updateServerInfo() async { Future<void> _updateServerInfo() async {
var serverInfo = await _serverInfoService.getServerInfo(); final serverInfo = await _serverInfoService.getServerInfo();
// Update server info // Update server info
if (serverInfo != null) { if (serverInfo != null) {
@ -559,7 +544,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Future<void> _resumeBackup() async { Future<void> _resumeBackup() async {
// Check if user is login // 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 // User has been logged out return
if (accessKey == null || !_authState.isAuthenticated) { if (accessKey == null || !_authState.isAuthenticated) {
@ -590,65 +575,56 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} }
Future<void> resumeBackup() async { Future<void> resumeBackup() async {
// assumes the background service is currently running final List<BackupAlbum> selectedBackupAlbums = await _db.backupAlbums
// if true, waits until it has stopped to update the app state from HiveDB .filter()
// before actually resuming backup by calling the internal `_resumeBackup` .selectionEqualTo(BackupSelection.select)
final BackUpProgressEnum previous = state.backupProgress; .findAll();
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground); final List<BackupAlbum> excludedBackupAlbums = await _db.backupAlbums
final bool hasLock = await _backgroundService.acquireLock(); .filter()
if (!hasLock) { .selectionEqualTo(BackupSelection.select)
log.warning("WARNING [resumeBackup] failed to acquireLock"); .findAll();
return;
}
await Future.wait([
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
Hive.openBox(backgroundBackupInfoBox),
]);
final HiveBackupAlbums? albums =
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums; Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums; Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
if (albums != null) { if (selectedAlbums.isNotEmpty) {
if (selectedAlbums.isNotEmpty) { selectedAlbums = _updateAlbumsBackupTime(
selectedAlbums = _updateAlbumsBackupTime( selectedAlbums,
selectedAlbums, selectedBackupAlbums,
albums.selectedAlbumIds, );
albums.lastSelectedBackupTime,
);
}
if (excludedAlbums.isNotEmpty) {
excludedAlbums = _updateAlbumsBackupTime(
excludedAlbums,
albums.excludedAlbumsIds,
albums.lastExcludedBackupTime,
);
}
} }
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
if (excludedAlbums.isNotEmpty) {
excludedAlbums = _updateAlbumsBackupTime(
excludedAlbums,
excludedBackupAlbums,
);
}
final BackUpProgressEnum previous = state.backupProgress;
state = state.copyWith( state = state.copyWith(
backupProgress: previous, backupProgress: BackUpProgressEnum.inBackground,
selectedBackupAlbums: selectedAlbums, selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums, excludedBackupAlbums: excludedAlbums,
backupRequireWifi: backgroundBox.get(backupRequireWifi), backupRequireWifi: Store.get(StoreKey.backupRequireWifi),
backupRequireCharging: backgroundBox.get(backupRequireCharging), backupRequireCharging: Store.get(StoreKey.backupRequireCharging),
backupTriggerDelay: backgroundBox.get(backupTriggerDelay), 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(); return _resumeBackup();
} }
Set<AvailableAlbum> _updateAlbumsBackupTime( Set<AvailableAlbum> _updateAlbumsBackupTime(
Set<AvailableAlbum> albums, Set<AvailableAlbum> albums,
List<String> ids, List<BackupAlbum> backupAlbums,
List<DateTime> times,
) { ) {
Set<AvailableAlbum> result = {}; Set<AvailableAlbum> result = {};
for (int i = 0; i < ids.length; i++) { for (BackupAlbum ba in backupAlbums) {
try { try {
AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]); AvailableAlbum a = albums.firstWhere((e) => e.id == ba.id);
result.add(a.copyWith(lastBackup: times[i])); result.add(a.copyWith(lastBackup: ba.lastBackup));
} on StateError { } on StateError {
log.severe( log.severe(
"[_updateAlbumBackupTime] failed to find album in state", "[_updateAlbumBackupTime] failed to find album in state",
@ -667,35 +643,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
AppStateEnum.detached, AppStateEnum.detached,
]; ];
if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) { if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
try {
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
}
} 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(); _backgroundService.releaseLock();
} }
} }
@ -709,6 +656,7 @@ final backupProvider =
ref.watch(authenticationProvider), ref.watch(authenticationProvider),
ref.watch(backgroundServiceProvider), ref.watch(backgroundServiceProvider),
ref.watch(galleryPermissionNotifier.notifier), ref.watch(galleryPermissionNotifier.notifier),
ref.watch(dbProvider),
ref, ref,
); );
}); });

View File

@ -8,31 +8,34 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.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/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/shared/providers/api.provider.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/shared/services/api.service.dart';
import 'package:immich_mobile/utils/files_helper.dart'; import 'package:immich_mobile/utils/files_helper.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:cancellation_token_http/http.dart' as http; import 'package:cancellation_token_http/http.dart' as http;
import '../models/hive_duplicated_assets.model.dart';
final backupServiceProvider = Provider( final backupServiceProvider = Provider(
(ref) => BackupService( (ref) => BackupService(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(dbProvider),
), ),
); );
class BackupService { class BackupService {
final httpClient = http.Client(); final httpClient = http.Client();
final ApiService _apiService; final ApiService _apiService;
final Isar _db;
BackupService(this._apiService); BackupService(this._apiService, this._db);
Future<List<String>?> getDeviceBackupAsset() async { Future<List<String>?> getDeviceBackupAsset() async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey); String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
@ -45,32 +48,28 @@ class BackupService {
} }
} }
void _saveDuplicatedAssetIdToLocalStorage(List<String> deviceAssetIds) { Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) {
HiveDuplicatedAssets duplicatedAssets = final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList();
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates));
.get(duplicatedAssetsKey) ??
HiveDuplicatedAssets(duplicatedAssetIds: []);
duplicatedAssets.duplicatedAssetIds =
{...duplicatedAssets.duplicatedAssetIds, ...deviceAssetIds}.toList();
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
.put(duplicatedAssetsKey, duplicatedAssets);
} }
/// Get duplicated asset id from Hive storage /// Get duplicated asset id from database
Set<String> getDuplicatedAssetIds() { Future<Set<String>> getDuplicatedAssetIds() async {
HiveDuplicatedAssets duplicatedAssets = final duplicates = await _db.duplicatedAssets.where().findAll();
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox) return duplicates.map((e) => e.id).toSet();
.get(duplicatedAssetsKey) ??
HiveDuplicatedAssets(duplicatedAssetIds: []);
return duplicatedAssets.duplicatedAssetIds.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 /// Returns all assets newer than the last successful backup per album
Future<List<AssetEntity>> buildUploadCandidates( Future<List<AssetEntity>> buildUploadCandidates(
HiveBackupAlbums backupAlbums, List<BackupAlbum> selectedBackupAlbums,
List<BackupAlbum> excludedBackupAlbums,
) async { ) async {
final filter = FilterOptionGroup( final filter = FilterOptionGroup(
containsPathModified: true, containsPathModified: true,
@ -81,66 +80,55 @@ class BackupService {
); );
final now = DateTime.now(); final now = DateTime.now();
final List<AssetPathEntity?> selectedAlbums = final List<AssetPathEntity?> selectedAlbums =
await _loadAlbumsWithTimeFilter( await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now);
backupAlbums.selectedAlbumIds,
backupAlbums.lastSelectedBackupTime,
filter,
now,
);
if (selectedAlbums.every((e) => e == null)) { if (selectedAlbums.every((e) => e == null)) {
return []; return [];
} }
final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll); final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll);
if (allIdx != -1) { if (allIdx != -1) {
final List<AssetPathEntity?> excludedAlbums = final List<AssetPathEntity?> excludedAlbums =
await _loadAlbumsWithTimeFilter( await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now);
backupAlbums.excludedAlbumsIds,
backupAlbums.lastExcludedBackupTime,
filter,
now,
);
final List<AssetEntity> toAdd = await _fetchAssetsAndUpdateLastBackup( final List<AssetEntity> toAdd = await _fetchAssetsAndUpdateLastBackup(
selectedAlbums.slice(allIdx, allIdx + 1), selectedAlbums.slice(allIdx, allIdx + 1),
backupAlbums.lastSelectedBackupTime.slice(allIdx, allIdx + 1), selectedBackupAlbums.slice(allIdx, allIdx + 1),
now, now,
); );
final List<AssetEntity> toRemove = await _fetchAssetsAndUpdateLastBackup( final List<AssetEntity> toRemove = await _fetchAssetsAndUpdateLastBackup(
excludedAlbums, excludedAlbums,
backupAlbums.lastExcludedBackupTime, excludedBackupAlbums,
now, now,
); );
return toAdd.toSet().difference(toRemove.toSet()).toList(); return toAdd.toSet().difference(toRemove.toSet()).toList();
} else { } else {
return await _fetchAssetsAndUpdateLastBackup( return await _fetchAssetsAndUpdateLastBackup(
selectedAlbums, selectedAlbums,
backupAlbums.lastSelectedBackupTime, selectedBackupAlbums,
now, now,
); );
} }
} }
Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter( Future<List<AssetPathEntity?>> _loadAlbumsWithTimeFilter(
List<String> albumIds, List<BackupAlbum> albums,
List<DateTime> lastBackups,
FilterOptionGroup filter, FilterOptionGroup filter,
DateTime now, DateTime now,
) async { ) async {
List<AssetPathEntity?> result = List.filled(albumIds.length, null); List<AssetPathEntity?> result = [];
for (int i = 0; i < albumIds.length; i++) { for (BackupAlbum a in albums) {
try { try {
final AssetPathEntity album = final AssetPathEntity album =
await AssetPathEntity.obtainPathFromProperties( await AssetPathEntity.obtainPathFromProperties(
id: albumIds[i], id: a.id,
optionGroup: filter.copyWith( optionGroup: filter.copyWith(
updateTimeCond: DateTimeCond( updateTimeCond: DateTimeCond(
// subtract 2 seconds to prevent missing assets due to rounding issues // 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, max: now,
), ),
), ),
maxDateTimeToNow: false, maxDateTimeToNow: false,
); );
result[i] = album; result.add(album);
} on StateError { } on StateError {
// either there are no assets matching the filter criteria OR the album no longer exists // 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( Future<List<AssetEntity>> _fetchAssetsAndUpdateLastBackup(
List<AssetPathEntity?> albums, List<AssetPathEntity?> albums,
List<DateTime> lastBackup, List<BackupAlbum> backupAlbums,
DateTime now, DateTime now,
) async { ) async {
List<AssetEntity> result = []; List<AssetEntity> result = [];
for (int i = 0; i < albums.length; i++) { for (int i = 0; i < albums.length; i++) {
final AssetPathEntity? a = albums[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( result.addAll(
await a.getAssetListRange(start: 0, end: await a.assetCountAsync), await a.getAssetListRange(start: 0, end: await a.assetCountAsync),
); );
lastBackup[i] = now; backupAlbums[i].lastBackup = now;
} }
} }
return result; return result;
@ -173,7 +162,7 @@ class BackupService {
if (candidates.isEmpty) { if (candidates.isEmpty) {
return candidates; return candidates;
} }
final Set<String> duplicatedAssetIds = getDuplicatedAssetIds(); final Set<String> duplicatedAssetIds = await getDuplicatedAssetIds();
candidates = duplicatedAssetIds.isEmpty candidates = duplicatedAssetIds.isEmpty
? candidates ? candidates
: candidates : candidates
@ -261,7 +250,8 @@ class BackupService {
req.fields['deviceId'] = deviceId; req.fields['deviceId'] = deviceId;
req.fields['assetType'] = _getAssetType(entity.type); req.fields['assetType'] = _getAssetType(entity.type);
req.fields['fileCreatedAt'] = entity.createDateTime.toIso8601String(); 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['isFavorite'] = entity.isFavorite.toString();
req.fields['fileExtension'] = fileExtension; req.fields['fileExtension'] = fileExtension;
req.fields['duration'] = entity.videoDuration.toString(); req.fields['duration'] = entity.videoDuration.toString();
@ -332,7 +322,7 @@ class BackupService {
} }
} }
if (duplicatedAssetIds.isNotEmpty) { if (duplicatedAssetIds.isNotEmpty) {
_saveDuplicatedAssetIdToLocalStorage(duplicatedAssetIds); await _saveDuplicatedAssetIds(duplicatedAssetIds);
} }
return !anyErrors; return !anyErrors;
} }

View File

@ -29,8 +29,8 @@ class BackupControllerPage extends HookConsumerWidget {
AuthenticationState authenticationState = ref.watch(authenticationProvider); AuthenticationState authenticationState = ref.watch(authenticationProvider);
final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings; final settings = ref.watch(iOSBackgroundSettingsProvider.notifier).settings;
final appRefreshDisabled = Platform.isIOS && final appRefreshDisabled =
settings?.appRefreshEnabled != true; Platform.isIOS && settings?.appRefreshEnabled != true;
bool hasExclusiveAccess = bool hasExclusiveAccess =
backupState.backupProgress != BackUpProgressEnum.inBackground; backupState.backupProgress != BackUpProgressEnum.inBackground;
bool shouldBackup = backupState.allUniqueAssets.length - bool shouldBackup = backupState.allUniqueAssets.length -
@ -292,15 +292,13 @@ class BackupControllerPage extends HookConsumerWidget {
dense: true, dense: true,
activeColor: activeColor, activeColor: activeColor,
value: isWifiRequired, value: isWifiRequired,
onChanged: hasExclusiveAccess onChanged: (isChecked) => ref
? (isChecked) => ref .read(backupProvider.notifier)
.read(backupProvider.notifier) .configureBackgroundBackup(
.configureBackgroundBackup( requireWifi: isChecked,
requireWifi: isChecked, onError: showErrorToUser,
onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser, ),
)
: null,
), ),
if (isBackgroundEnabled) if (isBackgroundEnabled)
SwitchListTile.adaptive( SwitchListTile.adaptive(
@ -314,21 +312,18 @@ class BackupControllerPage extends HookConsumerWidget {
dense: true, dense: true,
activeColor: activeColor, activeColor: activeColor,
value: isChargingRequired, value: isChargingRequired,
onChanged: hasExclusiveAccess onChanged: (isChecked) => ref
? (isChecked) => ref .read(backupProvider.notifier)
.read(backupProvider.notifier) .configureBackgroundBackup(
.configureBackgroundBackup( requireCharging: isChecked,
requireCharging: isChecked, onError: showErrorToUser,
onError: showErrorToUser, onBatteryInfo: showBatteryOptimizationInfoToUser,
onBatteryInfo: showBatteryOptimizationInfoToUser, ),
)
: null,
), ),
if (isBackgroundEnabled && Platform.isAndroid) if (isBackgroundEnabled && Platform.isAndroid)
ListTile( ListTile(
isThreeLine: false, isThreeLine: false,
dense: true, dense: true,
enabled: hasExclusiveAccess,
title: const Text( title: const Text(
'backup_controller_page_background_delay', 'backup_controller_page_background_delay',
style: TextStyle( style: TextStyle(
@ -339,9 +334,7 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
subtitle: Slider( subtitle: Slider(
value: triggerDelay.value, value: triggerDelay.value,
onChanged: hasExclusiveAccess onChanged: (double v) => triggerDelay.value = v,
? (double v) => triggerDelay.value = v
: null,
onChangeEnd: (double v) => ref onChangeEnd: (double v) => ref
.read(backupProvider.notifier) .read(backupProvider.notifier)
.configureBackgroundBackup( .configureBackgroundBackup(
@ -379,15 +372,13 @@ class BackupControllerPage extends HookConsumerWidget {
if (isBackgroundEnabled && Platform.isIOS) if (isBackgroundEnabled && Platform.isIOS)
FutureBuilder( FutureBuilder(
future: ref future: ref
.read(backgroundServiceProvider) .read(backgroundServiceProvider)
.getIOSBackgroundAppRefreshEnabled(), .getIOSBackgroundAppRefreshEnabled(),
builder: (context, snapshot) { builder: (context, snapshot) {
final enabled = snapshot.data as bool?; final enabled = snapshot.data as bool?;
// If it's not enabled, show them some kind of alert that says // If it's not enabled, show them some kind of alert that says
// background refresh is not enabled // background refresh is not enabled
if (enabled != null && !enabled) { if (enabled != null && !enabled) {}
}
// If it's enabled, no need to bother them // If it's enabled, no need to bother them
return Container(); return Container();
}, },
@ -395,7 +386,7 @@ class BackupControllerPage extends HookConsumerWidget {
if (Platform.isIOS && isBackgroundEnabled && settings != null) if (Platform.isIOS && isBackgroundEnabled && settings != null)
IosDebugInfoTile( IosDebugInfoTile(
settings: settings, settings: settings,
), ),
], ],
); );
} }
@ -403,7 +394,9 @@ class BackupControllerPage extends HookConsumerWidget {
Widget buildBackgroundAppRefreshWarning() { Widget buildBackgroundAppRefreshWarning() {
return ListTile( return ListTile(
isThreeLine: true, isThreeLine: true,
leading: const Icon(Icons.task_outlined,), leading: const Icon(
Icons.task_outlined,
),
title: const Text( title: const Text(
'backup_controller_page_background_app_refresh_disabled_title', 'backup_controller_page_background_app_refresh_disabled_title',
style: TextStyle( style: TextStyle(
@ -420,7 +413,7 @@ class BackupControllerPage extends HookConsumerWidget {
'backup_controller_page_background_app_refresh_disabled_content', 'backup_controller_page_background_app_refresh_disabled_content',
).tr(), ).tr(),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => openAppSettings(), onPressed: () => openAppSettings(),
child: const Text( child: const Text(
'backup_controller_page_background_app_refresh_enable_button_text', 'backup_controller_page_background_app_refresh_enable_button_text',
@ -533,12 +526,9 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
), ),
trailing: ElevatedButton( trailing: ElevatedButton(
onPressed: hasExclusiveAccess onPressed: () {
? () { AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
AutoRouter.of(context) },
.push(const BackupAlbumSelectionRoute());
}
: null,
child: const Text( child: const Text(
"backup_controller_page_select", "backup_controller_page_select",
style: TextStyle( style: TextStyle(
@ -598,28 +588,12 @@ class BackupControllerPage extends HookConsumerWidget {
} }
buildBackgroundBackupInfo() { buildBackgroundBackupInfo() {
return hasExclusiveAccess return const ListTile(
? const SizedBox.shrink() leading: Icon(Icons.info_outline_rounded),
: Card( title: Text(
shape: RoundedRectangleBorder( "Background backup is currently running, cannot start manual backup",
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 Scaffold( return Scaffold(
@ -652,7 +626,6 @@ class BackupControllerPage extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
).tr(), ).tr(),
), ),
buildBackgroundBackupInfo(),
buildFolderSelectionTile(), buildFolderSelectionTile(),
BackupInfoCard( BackupInfoCard(
title: "backup_controller_page_total".tr(), title: "backup_controller_page_total".tr(),
@ -681,22 +654,20 @@ class BackupControllerPage extends HookConsumerWidget {
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
child: Platform.isIOS child: Platform.isIOS
? ( ? (appRefreshDisabled
appRefreshDisabled ? buildBackgroundAppRefreshWarning()
? buildBackgroundAppRefreshWarning() : buildBackgroundBackupController())
: buildBackgroundBackupController() : buildBackgroundBackupController(),
) : buildBackgroundBackupController(),
), ),
const Divider(), const Divider(),
buildStorageInformation(), buildStorageInformation(),
const Divider(), const Divider(),
const CurrentUploadingAssetInfoBox(), const CurrentUploadingAssetInfoBox(),
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
buildBackupButton() buildBackupButton()
], ],
), ),
), ),
); );
} }
} }

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'dart:convert'; import 'dart:convert';
@ -9,7 +10,8 @@ part 'store.g.dart';
/// Can be used concurrently from multiple isolates /// Can be used concurrently from multiple isolates
class Store { class Store {
static late final Isar _db; 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) /// Initializes the store (call exactly once per app start)
static void init(Isar db) { static void init(Isar db) {
@ -70,23 +72,44 @@ class StoreValue {
int? intValue; int? intValue;
String? strValue; String? strValue;
T? _extract<T>(StoreKey key) => key.isInt dynamic _extract(StoreKey key) {
? (key.fromDb == null ? intValue : key.fromDb!.call(Store._db, intValue!)) switch (key.type) {
: (key.fromJson != null case int:
? key.fromJson!(json.decode(strValue!)) return key.fromDb == null
: strValue); ? intValue
static Future<StoreValue> _of(dynamic value, StoreKey key) async => : key.fromDb!.call(Store._db, intValue!);
StoreValue( case bool:
key.id, return intValue == null ? null : intValue! == 1;
intValue: key.isInt case DateTime:
? (key.toDb == null return intValue == null
? value
: await key.toDb!.call(Store._db, value))
: null,
strValue: key.isInt
? 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`. /// Key for each possible value in the `Store`.
@ -94,21 +117,24 @@ class StoreValue {
enum StoreKey { enum StoreKey {
userRemoteId(0), userRemoteId(0),
assetETag(1), assetETag(1),
currentUser(2, isInt: true, fromDb: _getUser, toDb: _toUser), currentUser(2, type: int, fromDb: _getUser, toDb: _toUser),
deviceIdHash(3, isInt: true), deviceIdHash(3, type: int),
deviceId(4), deviceId(4),
; backupFailedSince(5, type: DateTime),
backupRequireWifi(6, type: bool),
backupRequireCharging(7, type: bool),
backupTriggerDelay(8, type: int);
const StoreKey( const StoreKey(
this.id, { this.id, {
this.isInt = false, this.type = String,
this.fromDb, this.fromDb,
this.toDb, this.toDb,
// ignore: unused_element // ignore: unused_element
this.fromJson, this.fromJson,
}); });
final int id; final int id;
final bool isInt; final Type type;
final dynamic Function(Isar, int)? fromDb; final dynamic Function(Isar, int)? fromDb;
final Future<int> Function(Isar, dynamic)? toDb; final Future<int> Function(Isar, dynamic)? toDb;
final Function(dynamic)? fromJson; final Function(dynamic)? fromJson;

View File

@ -4,22 +4,102 @@ import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.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/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/models/store.dart';
import 'package:immich_mobile/shared/services/asset_cache.service.dart'; import 'package:immich_mobile/shared/services/asset_cache.service.dart';
import 'package:isar/isar.dart';
Future<void> migrateHiveToStoreIfNecessary() async { 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 { try {
if (await Hive.boxExists(userInfoBox)) { if (await Hive.boxExists(boxName)) {
final Box box = await Hive.openBox(userInfoBox); await migrate(await Hive.openBox<T>(boxName));
await _migrateSingleKey(box, userIdKey, StoreKey.userRemoteId);
await _migrateSingleKey(box, assetEtagKey, StoreKey.assetETag);
} }
} catch (e) { } 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); final String? value = box.get(hiveKey);
if (value != null) { if (value != null) {
await Store.put(key, value); await Store.put(key, value);