mirror of
https://github.com/immich-app/immich.git
synced 2025-04-14 12:08:54 +02:00
refactor(mobile): DB repository for asset, backup, sync service (#12953)
* refactor(mobile): DB repository for asset, backup, sync service * review feedback * fix bug found by Alex --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
a2d457b01d
commit
15c04d3056
@ -64,19 +64,19 @@ custom_lint:
|
||||
allowed:
|
||||
# required / wanted
|
||||
- lib/entities/*.entity.dart
|
||||
- lib/repositories/{album,asset,backup,exif_info,user}.repository.dart
|
||||
# acceptable exceptions for the time being
|
||||
- lib/repositories/{album,asset,backup,database,etag,exif_info,user}.repository.dart
|
||||
# acceptable exceptions for the time being (until Isar is fully replaced)
|
||||
- integration_test/test_utils/general_helper.dart
|
||||
- lib/main.dart
|
||||
- lib/routing/router.dart
|
||||
- lib/utils/{db,migration,renderlist_generator}.dart
|
||||
- test/**.dart
|
||||
# refactor to make the providers and services testable
|
||||
- lib/pages/common/album_asset_selection.page.dart
|
||||
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
|
||||
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart
|
||||
- lib/services/{asset,background,backup,immich_logger,sync}.service.dart
|
||||
- lib/routing/router.dart
|
||||
- lib/services/immich_logger.service.dart # not really a service... more a util
|
||||
- lib/utils/{db,migration,renderlist_generator}.dart
|
||||
- lib/widgets/asset_grid/asset_grid_data_structure.dart
|
||||
- test/**.dart
|
||||
# refactor the remaining providers
|
||||
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
|
||||
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
|
||||
|
||||
- import_rule_openapi:
|
||||
message: openapi must only be used through ApiRepositories
|
||||
|
@ -1,21 +1,43 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IAlbumRepository {
|
||||
Future<int> count({bool? local});
|
||||
abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||
Future<Album> create(Album album);
|
||||
Future<Album?> getById(int id);
|
||||
|
||||
Future<Album?> get(int id);
|
||||
|
||||
Future<Album?> getByName(
|
||||
String name, {
|
||||
bool? shared,
|
||||
bool? remote,
|
||||
});
|
||||
|
||||
Future<List<Album>> getAll({
|
||||
bool? shared,
|
||||
bool? remote,
|
||||
int? ownerId,
|
||||
AlbumSort? sortBy,
|
||||
});
|
||||
|
||||
Future<Album> update(Album album);
|
||||
|
||||
Future<void> delete(int albumId);
|
||||
Future<List<Album>> getAll({bool? shared});
|
||||
|
||||
Future<void> deleteAllLocal();
|
||||
|
||||
Future<int> count({bool? local});
|
||||
|
||||
Future<void> addUsers(Album album, List<User> users);
|
||||
|
||||
Future<void> removeUsers(Album album, List<User> users);
|
||||
|
||||
Future<void> addAssets(Album album, List<Asset> assets);
|
||||
|
||||
Future<void> removeAssets(Album album, List<Asset> assets);
|
||||
|
||||
Future<Album> recalculateMetadata(Album album);
|
||||
}
|
||||
|
||||
enum AlbumSort { remoteId, localId }
|
||||
|
@ -1,27 +1,62 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IAssetRepository {
|
||||
abstract interface class IAssetRepository implements IDatabaseRepository {
|
||||
Future<Asset?> getByRemoteId(String id);
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
|
||||
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum);
|
||||
|
||||
Future<List<Asset>> getAllByRemoteId(
|
||||
Iterable<String> ids, {
|
||||
AssetState? state,
|
||||
});
|
||||
|
||||
Future<List<Asset?>> getAllByOwnerIdChecksum(
|
||||
List<int> ids,
|
||||
List<String> checksums,
|
||||
);
|
||||
|
||||
Future<List<Asset>> getAll({
|
||||
required int ownerId,
|
||||
bool? remote,
|
||||
int limit = 100,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
int? limit,
|
||||
});
|
||||
|
||||
Future<List<Asset>> getAllLocal();
|
||||
|
||||
Future<List<Asset>> getByAlbum(
|
||||
Album album, {
|
||||
Iterable<int> notOwnedBy = const [],
|
||||
int? ownerId,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
});
|
||||
|
||||
Future<Asset> update(Asset asset);
|
||||
|
||||
Future<List<Asset>> updateAll(List<Asset> assets);
|
||||
|
||||
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
|
||||
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<List<Asset>> getMatches({
|
||||
required List<Asset> assets,
|
||||
required int ownerId,
|
||||
bool? remote,
|
||||
AssetState? state,
|
||||
int limit = 100,
|
||||
});
|
||||
|
||||
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
|
||||
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
|
||||
|
||||
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
|
||||
|
||||
Future<List<String>> getAllDuplicatedAssetIds();
|
||||
}
|
||||
|
||||
enum AssetSort { checksum, ownerIdChecksum }
|
||||
|
@ -1,5 +1,16 @@
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IBackupRepository implements IDatabaseRepository {
|
||||
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort});
|
||||
|
||||
abstract interface class IBackupRepository {
|
||||
Future<List<String>> getIdsBySelection(BackupSelection backup);
|
||||
|
||||
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup);
|
||||
|
||||
Future<void> updateAll(List<BackupAlbum> backupAlbums);
|
||||
|
||||
Future<void> deleteAll(List<int> ids);
|
||||
}
|
||||
|
||||
enum BackupAlbumSort { id }
|
||||
|
3
mobile/lib/interfaces/database.interface.dart
Normal file
3
mobile/lib/interfaces/database.interface.dart
Normal file
@ -0,0 +1,3 @@
|
||||
abstract interface class IDatabaseRepository {
|
||||
Future<T> transaction<T>(Future<T> Function() callback);
|
||||
}
|
14
mobile/lib/interfaces/etag.interface.dart
Normal file
14
mobile/lib/interfaces/etag.interface.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IETagRepository implements IDatabaseRepository {
|
||||
Future<ETag?> get(int id);
|
||||
|
||||
Future<ETag?> getById(String id);
|
||||
|
||||
Future<List<String>> getAllIds();
|
||||
|
||||
Future<void> upsertAll(List<ETag> etags);
|
||||
|
||||
Future<void> deleteByIds(List<String> ids);
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IExifInfoRepository {
|
||||
abstract interface class IExifInfoRepository implements IDatabaseRepository {
|
||||
Future<ExifInfo?> get(int id);
|
||||
|
||||
Future<ExifInfo> update(ExifInfo exifInfo);
|
||||
|
||||
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
|
||||
|
||||
Future<void> delete(int id);
|
||||
}
|
||||
|
@ -1,8 +1,23 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IUserRepository {
|
||||
Future<List<User>> getByIds(List<String> ids);
|
||||
abstract interface class IUserRepository implements IDatabaseRepository {
|
||||
Future<User?> get(String id);
|
||||
Future<List<User>> getAll({bool self = true});
|
||||
|
||||
Future<List<User>> getByIds(List<String> ids);
|
||||
|
||||
Future<List<User>> getAll({bool self = true, UserSort? sortBy});
|
||||
|
||||
/// Returns all users whose assets can be accessed (self+partners)
|
||||
Future<List<User>> getAllAccessible();
|
||||
|
||||
Future<List<User>> upsertAll(List<User> users);
|
||||
|
||||
Future<User> update(User user);
|
||||
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<User> me();
|
||||
}
|
||||
|
||||
enum UserSort { id }
|
||||
|
@ -275,28 +275,14 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
return isSuccess ? remote.toList() : [];
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async {
|
||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
|
||||
status ??= !assets.every((a) => a.isFavorite);
|
||||
final newAssets = await _assetService.changeFavoriteStatus(assets, status);
|
||||
for (Asset? newAsset in newAssets) {
|
||||
if (newAsset == null) {
|
||||
log.severe("Change favorite status failed for asset");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return _assetService.changeFavoriteStatus(assets, status);
|
||||
}
|
||||
|
||||
Future<void> toggleArchive(List<Asset> assets, [bool? status]) async {
|
||||
Future<void> toggleArchive(List<Asset> assets, [bool? status]) {
|
||||
status ??= !assets.every((a) => a.isArchived);
|
||||
final newAssets = await _assetService.changeArchiveStatus(assets, status);
|
||||
int i = 0;
|
||||
for (Asset oldAsset in assets) {
|
||||
final newAsset = newAssets[i++];
|
||||
if (newAsset == null) {
|
||||
log.severe("Change archive status failed for asset ${oldAsset.id}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return _assetService.changeArchiveStatus(assets, status);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||
import 'package:immich_mobile/models/backup/available_album.model.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
@ -45,6 +47,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
this._db,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
this._backupRepository,
|
||||
this.ref,
|
||||
) : super(
|
||||
BackUpState(
|
||||
@ -95,6 +98,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
final Isar _db;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final IBackupRepository _backupRepository;
|
||||
final Ref ref;
|
||||
|
||||
///
|
||||
@ -255,9 +259,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
|
||||
final List<BackupAlbum> excludedBackupAlbums =
|
||||
await _backupService.excludedAlbumsQuery().findAll();
|
||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
final List<BackupAlbum> selectedBackupAlbums =
|
||||
await _backupService.selectedAlbumsQuery().findAll();
|
||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
||||
|
||||
final Set<AvailableAlbum> selectedAlbums = {};
|
||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||
@ -767,6 +771,7 @@ final backupProvider =
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
@ -6,8 +6,10 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
@ -25,7 +27,6 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
@ -36,6 +37,7 @@ final manualUploadProvider =
|
||||
ref.watch(localNotificationService),
|
||||
ref.watch(backupProvider.notifier),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
@ -45,12 +47,14 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
final LocalNotificationService _localNotificationService;
|
||||
final BackupNotifier _backupProvider;
|
||||
final BackupService _backupService;
|
||||
final BackupRepository _backupRepository;
|
||||
final Ref ref;
|
||||
|
||||
ManualUploadNotifier(
|
||||
this._localNotificationService,
|
||||
this._backupProvider,
|
||||
this._backupService,
|
||||
this._backupRepository,
|
||||
this.ref,
|
||||
) : super(
|
||||
ManualUploadState(
|
||||
@ -206,9 +210,9 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
}
|
||||
|
||||
final selectedBackupAlbums =
|
||||
_backupService.selectedAlbumsQuery().findAllSync();
|
||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
||||
final excludedBackupAlbums =
|
||||
_backupService.excludedAlbumsQuery().findAllSync();
|
||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
|
||||
// Get candidates from selected albums and excluded albums
|
||||
Set<BackupCandidate> candidates =
|
||||
|
@ -3,14 +3,14 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final activityApiRepositoryProvider = Provider(
|
||||
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
|
||||
);
|
||||
|
||||
class ActivityApiRepository extends BaseApiRepository
|
||||
class ActivityApiRepository extends ApiRepository
|
||||
implements IActivityApiRepository {
|
||||
final ActivitiesApi _api;
|
||||
|
||||
|
@ -4,32 +4,36 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final albumRepositoryProvider =
|
||||
Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
|
||||
|
||||
class AlbumRepository implements IAlbumRepository {
|
||||
final Isar _db;
|
||||
|
||||
AlbumRepository(
|
||||
this._db,
|
||||
);
|
||||
class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
AlbumRepository(super.db);
|
||||
|
||||
@override
|
||||
Future<int> count({bool? local}) {
|
||||
if (local == true) return _db.albums.where().localIdIsNotNull().count();
|
||||
if (local == false) return _db.albums.where().remoteIdIsNotNull().count();
|
||||
return _db.albums.count();
|
||||
final baseQuery = db.albums.where();
|
||||
final QueryBuilder<Album, Album, QAfterWhereClause> query;
|
||||
switch (local) {
|
||||
case null:
|
||||
query = baseQuery.noOp();
|
||||
case true:
|
||||
query = baseQuery.localIdIsNotNull();
|
||||
case false:
|
||||
query = baseQuery.remoteIdIsNotNull();
|
||||
}
|
||||
return query.count();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Album album) =>
|
||||
_db.writeTxn(() => _db.albums.store(album));
|
||||
Future<Album> create(Album album) => txn(() => db.albums.store(album));
|
||||
|
||||
@override
|
||||
Future<Album?> getByName(String name, {bool? shared, bool? remote}) {
|
||||
var query = _db.albums.filter().nameEqualTo(name);
|
||||
var query = db.albums.filter().nameEqualTo(name);
|
||||
if (shared != null) {
|
||||
query = query.sharedEqualTo(shared);
|
||||
}
|
||||
@ -42,37 +46,61 @@ class AlbumRepository implements IAlbumRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> update(Album album) =>
|
||||
_db.writeTxn(() => _db.albums.store(album));
|
||||
Future<Album> update(Album album) => txn(() => db.albums.store(album));
|
||||
|
||||
@override
|
||||
Future<void> delete(int albumId) =>
|
||||
_db.writeTxn(() => _db.albums.delete(albumId));
|
||||
Future<void> delete(int albumId) => txn(() => db.albums.delete(albumId));
|
||||
|
||||
@override
|
||||
Future<List<Album>> getAll({bool? shared}) {
|
||||
final baseQuery = _db.albums.filter();
|
||||
QueryBuilder<Album, Album, QAfterFilterCondition>? query;
|
||||
if (shared != null) {
|
||||
query = baseQuery.sharedEqualTo(true);
|
||||
Future<List<Album>> getAll({
|
||||
bool? shared,
|
||||
bool? remote,
|
||||
int? ownerId,
|
||||
AlbumSort? sortBy,
|
||||
}) {
|
||||
final baseQuery = db.albums.where();
|
||||
final QueryBuilder<Album, Album, QAfterWhereClause> afterWhere;
|
||||
if (remote == null) {
|
||||
afterWhere = baseQuery.noOp();
|
||||
} else if (remote) {
|
||||
afterWhere = baseQuery.remoteIdIsNotNull();
|
||||
} else {
|
||||
afterWhere = baseQuery.localIdIsNotNull();
|
||||
}
|
||||
return query?.findAll() ?? _db.albums.where().findAll();
|
||||
QueryBuilder<Album, Album, QAfterFilterCondition> filterQuery =
|
||||
afterWhere.filter().noOp();
|
||||
if (shared != null) {
|
||||
filterQuery = filterQuery.sharedEqualTo(true);
|
||||
}
|
||||
if (ownerId != null) {
|
||||
filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId));
|
||||
}
|
||||
final QueryBuilder<Album, Album, QAfterSortBy> query;
|
||||
switch (sortBy) {
|
||||
case null:
|
||||
query = filterQuery.noOp();
|
||||
case AlbumSort.remoteId:
|
||||
query = filterQuery.sortByRemoteId();
|
||||
case AlbumSort.localId:
|
||||
query = filterQuery.sortByLocalId();
|
||||
}
|
||||
return query.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album?> getById(int id) => _db.albums.get(id);
|
||||
Future<Album?> get(int id) => db.albums.get(id);
|
||||
|
||||
@override
|
||||
Future<void> removeUsers(Album album, List<User> users) =>
|
||||
_db.writeTxn(() => album.sharedUsers.update(unlink: users));
|
||||
txn(() => album.sharedUsers.update(unlink: users));
|
||||
|
||||
@override
|
||||
Future<void> addAssets(Album album, List<Asset> assets) =>
|
||||
_db.writeTxn(() => album.assets.update(link: assets));
|
||||
txn(() => album.assets.update(link: assets));
|
||||
|
||||
@override
|
||||
Future<void> removeAssets(Album album, List<Asset> assets) =>
|
||||
_db.writeTxn(() => album.assets.update(unlink: assets));
|
||||
txn(() => album.assets.update(unlink: assets));
|
||||
|
||||
@override
|
||||
Future<Album> recalculateMetadata(Album album) async {
|
||||
@ -82,4 +110,12 @@ class AlbumRepository implements IAlbumRepository {
|
||||
await album.assets.filter().updatedAtProperty().max();
|
||||
return album;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addUsers(Album album, List<User> users) =>
|
||||
txn(() => album.sharedUsers.update(link: users));
|
||||
|
||||
@override
|
||||
Future<void> deleteAllLocal() =>
|
||||
txn(() => db.albums.where().localIdIsNotNull().deleteAll());
|
||||
}
|
||||
|
@ -4,15 +4,14 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final albumApiRepositoryProvider = Provider(
|
||||
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||
);
|
||||
|
||||
class AlbumApiRepository extends BaseApiRepository
|
||||
implements IAlbumApiRepository {
|
||||
class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
final AlbumsApi _api;
|
||||
|
||||
AlbumApiRepository(this._api);
|
||||
@ -26,7 +25,7 @@ class AlbumApiRepository extends BaseApiRepository
|
||||
@override
|
||||
Future<List<Album>> getAll({bool? shared}) async {
|
||||
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
|
||||
return dtos.map(_toAlbum).toList().cast();
|
||||
return dtos.map(_toAlbum).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1,8 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/constants/errors.dart';
|
||||
|
||||
abstract class BaseApiRepository {
|
||||
@protected
|
||||
abstract class ApiRepository {
|
||||
Future<T> checkNull<T>(Future<T?> future) async {
|
||||
final response = await future;
|
||||
if (response == null) throw NoResponseDtoError();
|
@ -5,78 +5,145 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final assetRepositoryProvider =
|
||||
Provider((ref) => AssetRepository(ref.watch(dbProvider)));
|
||||
|
||||
class AssetRepository implements IAssetRepository {
|
||||
final Isar _db;
|
||||
|
||||
AssetRepository(
|
||||
this._db,
|
||||
);
|
||||
class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
AssetRepository(super.db);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy}) {
|
||||
Future<List<Asset>> getByAlbum(
|
||||
Album album, {
|
||||
Iterable<int> notOwnedBy = const [],
|
||||
int? ownerId,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
}) {
|
||||
var query = album.assets.filter();
|
||||
if (notOwnedBy != null) {
|
||||
query = query.not().ownerIdEqualTo(notOwnedBy.isarId);
|
||||
if (notOwnedBy.length == 1) {
|
||||
query = query.not().ownerIdEqualTo(notOwnedBy.first);
|
||||
} else if (notOwnedBy.isNotEmpty) {
|
||||
query =
|
||||
query.not().anyOf(notOwnedBy, (q, int id) => q.ownerIdEqualTo(id));
|
||||
}
|
||||
return query.findAll();
|
||||
if (ownerId != null) {
|
||||
query = query.ownerIdEqualTo(ownerId);
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case null:
|
||||
break;
|
||||
case AssetState.local:
|
||||
query = query.remoteIdIsNull();
|
||||
case AssetState.remote:
|
||||
query = query.localIdIsNull();
|
||||
case AssetState.merged:
|
||||
query = query.localIdIsNotNull().remoteIdIsNotNull();
|
||||
}
|
||||
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy> sortedQuery;
|
||||
|
||||
switch (sortBy) {
|
||||
case null:
|
||||
sortedQuery = query.noOp();
|
||||
case AssetSort.checksum:
|
||||
sortedQuery = query.sortByChecksum();
|
||||
case AssetSort.ownerIdChecksum:
|
||||
sortedQuery = query.sortByOwnerId().thenByChecksum();
|
||||
}
|
||||
|
||||
return sortedQuery.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteById(List<int> ids) =>
|
||||
_db.writeTxn(() => _db.assets.deleteAll(ids));
|
||||
Future<void> deleteById(List<int> ids) => txn(() async {
|
||||
await db.assets.deleteAll(ids);
|
||||
await db.exifInfos.deleteAll(ids);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id);
|
||||
Future<Asset?> getByRemoteId(String id) => db.assets.getByRemoteId(id);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||
_db.assets.getAllByRemoteId(ids);
|
||||
Future<List<Asset>> getAllByRemoteId(
|
||||
Iterable<String> ids, {
|
||||
AssetState? state,
|
||||
}) =>
|
||||
_getAllByRemoteIdImpl(ids, state).findAll();
|
||||
|
||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> _getAllByRemoteIdImpl(
|
||||
Iterable<String> ids,
|
||||
AssetState? state,
|
||||
) {
|
||||
final query = db.assets.remote(ids).filter();
|
||||
switch (state) {
|
||||
case null:
|
||||
return query.noOp();
|
||||
case AssetState.local:
|
||||
return query.remoteIdIsNull();
|
||||
case AssetState.remote:
|
||||
return query.localIdIsNull();
|
||||
case AssetState.merged:
|
||||
return query.localIdIsNotEmpty().remoteIdIsNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAll({
|
||||
required int ownerId,
|
||||
bool? remote,
|
||||
int limit = 100,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
int? limit,
|
||||
}) {
|
||||
if (remote == null) {
|
||||
return _db.assets
|
||||
.where()
|
||||
.ownerIdEqualToAnyChecksum(ownerId)
|
||||
.limit(limit)
|
||||
.findAll();
|
||||
}
|
||||
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
|
||||
if (remote) {
|
||||
query = _db.assets
|
||||
.where()
|
||||
.localIdIsNull()
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
} else {
|
||||
query = _db.assets
|
||||
.where()
|
||||
.remoteIdIsNull()
|
||||
.filter()
|
||||
.localIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
final baseQuery = db.assets.where();
|
||||
final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery;
|
||||
switch (state) {
|
||||
case null:
|
||||
filteredQuery = baseQuery.ownerIdEqualToAnyChecksum(ownerId).noOp();
|
||||
case AssetState.local:
|
||||
filteredQuery = baseQuery
|
||||
.remoteIdIsNull()
|
||||
.filter()
|
||||
.localIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
case AssetState.remote:
|
||||
filteredQuery = baseQuery
|
||||
.localIdIsNull()
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
case AssetState.merged:
|
||||
filteredQuery = baseQuery
|
||||
.ownerIdEqualToAnyChecksum(ownerId)
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.localIdIsNotNull();
|
||||
}
|
||||
|
||||
return query.limit(limit).findAll();
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy> query;
|
||||
switch (sortBy) {
|
||||
case null:
|
||||
query = filteredQuery.noOp();
|
||||
case AssetSort.checksum:
|
||||
query = filteredQuery.sortByChecksum();
|
||||
case AssetSort.ownerIdChecksum:
|
||||
query = filteredQuery.sortByOwnerId().thenByChecksum();
|
||||
}
|
||||
|
||||
return limit == null ? query.findAll() : query.limit(limit).findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Asset>> updateAll(List<Asset> assets) async {
|
||||
await _db.writeTxn(() => _db.assets.putAll(assets));
|
||||
await txn(() => db.assets.putAll(assets));
|
||||
return assets;
|
||||
}
|
||||
|
||||
@ -84,16 +151,20 @@ class AssetRepository implements IAssetRepository {
|
||||
Future<List<Asset>> getMatches({
|
||||
required List<Asset> assets,
|
||||
required int ownerId,
|
||||
bool? remote,
|
||||
AssetState? state,
|
||||
int limit = 100,
|
||||
}) {
|
||||
final baseQuery = db.assets.where();
|
||||
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
|
||||
if (remote == null) {
|
||||
query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull();
|
||||
} else if (remote) {
|
||||
query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull();
|
||||
} else {
|
||||
query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull();
|
||||
switch (state) {
|
||||
case null:
|
||||
query = baseQuery.noOp();
|
||||
case AssetState.local:
|
||||
query = baseQuery.remoteIdIsNull().filter().localIdIsNotNull();
|
||||
case AssetState.remote:
|
||||
query = baseQuery.localIdIsNull().filter().remoteIdIsNotNull();
|
||||
case AssetState.merged:
|
||||
query = baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull();
|
||||
}
|
||||
return _getMatchesImpl(query, ownerId, assets, limit);
|
||||
}
|
||||
@ -101,16 +172,50 @@ class AssetRepository implements IAssetRepository {
|
||||
@override
|
||||
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
|
||||
Platform.isAndroid
|
||||
? _db.androidDeviceAssets.getAll(ids.cast())
|
||||
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
? db.androidDeviceAssets.getAll(ids.cast())
|
||||
: db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
|
||||
@override
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) =>
|
||||
_db.writeTxn(
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) => txn(
|
||||
() => Platform.isAndroid
|
||||
? _db.androidDeviceAssets.putAll(deviceAssets.cast())
|
||||
: _db.iOSDeviceAssets.putAll(deviceAssets.cast()),
|
||||
? db.androidDeviceAssets.putAll(deviceAssets.cast())
|
||||
: db.iOSDeviceAssets.putAll(deviceAssets.cast()),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Asset> update(Asset asset) async {
|
||||
await txn(() => asset.put(db));
|
||||
return asset;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets) => txn(
|
||||
() => db.duplicatedAssets
|
||||
.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList()),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<String>> getAllDuplicatedAssetIds() =>
|
||||
db.duplicatedAssets.where().idProperty().findAll();
|
||||
|
||||
@override
|
||||
Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum) =>
|
||||
db.assets.getByOwnerIdChecksum(ownerId, checksum);
|
||||
|
||||
@override
|
||||
Future<List<Asset?>> getAllByOwnerIdChecksum(
|
||||
List<int> ids,
|
||||
List<String> checksums,
|
||||
) =>
|
||||
db.assets.getAllByOwnerIdChecksum(ids, checksums);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAllLocal() =>
|
||||
db.assets.where().localIdIsNotNull().findAll();
|
||||
|
||||
@override
|
||||
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state}) =>
|
||||
txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll());
|
||||
}
|
||||
|
||||
Future<List<Asset>> _getMatchesImpl(
|
||||
|
@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final assetApiRepositoryProvider = Provider(
|
||||
@ -12,8 +12,7 @@ final assetApiRepositoryProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
class AssetApiRepository extends BaseApiRepository
|
||||
implements IAssetApiRepository {
|
||||
class AssetApiRepository extends ApiRepository implements IAssetApiRepository {
|
||||
final AssetsApi _api;
|
||||
final SearchApi _searchApi;
|
||||
|
||||
|
@ -2,19 +2,41 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final backupRepositoryProvider =
|
||||
Provider((ref) => BackupRepository(ref.watch(dbProvider)));
|
||||
|
||||
class BackupRepository implements IBackupRepository {
|
||||
final Isar _db;
|
||||
class BackupRepository extends DatabaseRepository implements IBackupRepository {
|
||||
BackupRepository(super.db);
|
||||
|
||||
BackupRepository(
|
||||
this._db,
|
||||
);
|
||||
@override
|
||||
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
||||
final baseQuery = db.backupAlbums.where();
|
||||
final QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> query;
|
||||
switch (sort) {
|
||||
case null:
|
||||
query = baseQuery.noOp();
|
||||
case BackupAlbumSort.id:
|
||||
query = baseQuery.sortById();
|
||||
}
|
||||
return query.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> getIdsBySelection(BackupSelection backup) =>
|
||||
_db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
|
||||
db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
|
||||
|
||||
@override
|
||||
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup) =>
|
||||
db.backupAlbums.filter().selectionEqualTo(backup).findAll();
|
||||
|
||||
@override
|
||||
Future<void> deleteAll(List<int> ids) =>
|
||||
txn(() => db.backupAlbums.deleteAll(ids));
|
||||
|
||||
@override
|
||||
Future<void> updateAll(List<BackupAlbum> backupAlbums) =>
|
||||
txn(() => db.backupAlbums.putAll(backupAlbums));
|
||||
}
|
||||
|
28
mobile/lib/repositories/database.repository.dart
Normal file
28
mobile/lib/repositories/database.repository.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
/// copied from Isar; needed to check if an async transaction is already active
|
||||
const Symbol _zoneTxn = #zoneTxn;
|
||||
|
||||
abstract class DatabaseRepository implements IDatabaseRepository {
|
||||
final Isar db;
|
||||
DatabaseRepository(this.db);
|
||||
|
||||
bool get inTxn => Zone.current[_zoneTxn] != null;
|
||||
|
||||
Future<T> txn<T>(Future<T> Function() callback) =>
|
||||
inTxn ? callback() : transaction(callback);
|
||||
|
||||
@override
|
||||
Future<T> transaction<T>(Future<T> Function() callback) =>
|
||||
db.writeTxn(callback);
|
||||
}
|
||||
|
||||
extension Asd<T> on QueryBuilder<T, dynamic, dynamic> {
|
||||
QueryBuilder<T, T, O> noOp<O>() {
|
||||
// ignore: invalid_use_of_protected_member
|
||||
return QueryBuilder.apply(this, (query) => query);
|
||||
}
|
||||
}
|
29
mobile/lib/repositories/etag.repository.dart
Normal file
29
mobile/lib/repositories/etag.repository.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final etagRepositoryProvider =
|
||||
Provider((ref) => ETagRepository(ref.watch(dbProvider)));
|
||||
|
||||
class ETagRepository extends DatabaseRepository implements IETagRepository {
|
||||
ETagRepository(super.db);
|
||||
|
||||
@override
|
||||
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
|
||||
|
||||
@override
|
||||
Future<ETag?> get(int id) => db.eTags.get(id);
|
||||
|
||||
@override
|
||||
Future<void> upsertAll(List<ETag> etags) => txn(() => db.eTags.putAll(etags));
|
||||
|
||||
@override
|
||||
Future<void> deleteByIds(List<String> ids) =>
|
||||
txn(() => db.eTags.deleteAllById(ids));
|
||||
|
||||
@override
|
||||
Future<ETag?> getById(String id) => db.eTags.getById(id);
|
||||
}
|
@ -2,27 +2,30 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
|
||||
final exifInfoRepositoryProvider =
|
||||
Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
|
||||
|
||||
class ExifInfoRepository implements IExifInfoRepository {
|
||||
final Isar _db;
|
||||
|
||||
ExifInfoRepository(
|
||||
this._db,
|
||||
);
|
||||
class ExifInfoRepository extends DatabaseRepository
|
||||
implements IExifInfoRepository {
|
||||
ExifInfoRepository(super.db);
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) => _db.exifInfos.delete(id);
|
||||
Future<void> delete(int id) => txn(() => db.exifInfos.delete(id));
|
||||
|
||||
@override
|
||||
Future<ExifInfo?> get(int id) => _db.exifInfos.get(id);
|
||||
Future<ExifInfo?> get(int id) => db.exifInfos.get(id);
|
||||
|
||||
@override
|
||||
Future<ExifInfo> update(ExifInfo exifInfo) async {
|
||||
await _db.writeTxn(() => _db.exifInfos.put(exifInfo));
|
||||
await txn(() => db.exifInfos.put(exifInfo));
|
||||
return exifInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) async {
|
||||
await txn(() => db.exifInfos.putAll(exifInfos));
|
||||
return exifInfos;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final partnerApiRepositoryProvider = Provider(
|
||||
@ -11,7 +11,7 @@ final partnerApiRepositoryProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
class PartnerApiRepository extends BaseApiRepository
|
||||
class PartnerApiRepository extends ApiRepository
|
||||
implements IPartnerApiRepository {
|
||||
final PartnersApi _api;
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final personApiRepositoryProvider = Provider(
|
||||
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
|
||||
);
|
||||
|
||||
class PersonApiRepository extends BaseApiRepository
|
||||
class PersonApiRepository extends ApiRepository
|
||||
implements IPersonApiRepository {
|
||||
final PeopleApi _api;
|
||||
|
||||
|
@ -3,37 +3,61 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final userRepositoryProvider =
|
||||
Provider((ref) => UserRepository(ref.watch(dbProvider)));
|
||||
|
||||
class UserRepository implements IUserRepository {
|
||||
final Isar _db;
|
||||
|
||||
UserRepository(
|
||||
this._db,
|
||||
);
|
||||
class UserRepository extends DatabaseRepository implements IUserRepository {
|
||||
UserRepository(super.db);
|
||||
|
||||
@override
|
||||
Future<List<User>> getByIds(List<String> ids) async =>
|
||||
(await _db.users.getAllById(ids)).cast();
|
||||
(await db.users.getAllById(ids)).nonNulls.toList();
|
||||
|
||||
@override
|
||||
Future<User?> get(String id) => _db.users.getById(id);
|
||||
Future<User?> get(String id) => db.users.getById(id);
|
||||
|
||||
@override
|
||||
Future<List<User>> getAll({bool self = true}) {
|
||||
if (self) {
|
||||
return _db.users.where().findAll();
|
||||
}
|
||||
Future<List<User>> getAll({bool self = true, UserSort? sortBy}) {
|
||||
final baseQuery = db.users.where();
|
||||
final int userId = Store.get(StoreKey.currentUser).isarId;
|
||||
return _db.users.where().isarIdNotEqualTo(userId).findAll();
|
||||
final QueryBuilder<User, User, QAfterWhereClause> afterWhere =
|
||||
self ? baseQuery.noOp() : baseQuery.isarIdNotEqualTo(userId);
|
||||
final QueryBuilder<User, User, QAfterSortBy> query;
|
||||
switch (sortBy) {
|
||||
case null:
|
||||
query = afterWhere.noOp();
|
||||
case UserSort.id:
|
||||
query = afterWhere.sortById();
|
||||
}
|
||||
return query.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> update(User user) async {
|
||||
await _db.writeTxn(() => _db.users.put(user));
|
||||
await txn(() => db.users.put(user));
|
||||
return user;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> me() => Future.value(Store.get(StoreKey.currentUser));
|
||||
|
||||
@override
|
||||
Future<void> deleteById(List<int> ids) => txn(() => db.users.deleteAll(ids));
|
||||
|
||||
@override
|
||||
Future<List<User>> upsertAll(List<User> users) async {
|
||||
await txn(() => db.users.putAll(users));
|
||||
return users;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<User>> getAllAccessible() => db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.or()
|
||||
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.findAll();
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import 'package:http/http.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/user_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final userApiRepositoryProvider = Provider(
|
||||
@ -14,8 +14,7 @@ final userApiRepositoryProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
class UserApiRepository extends BaseApiRepository
|
||||
implements IUserApiRepository {
|
||||
class UserApiRepository extends ApiRepository implements IUserApiRepository {
|
||||
final UsersApi _api;
|
||||
|
||||
UserApiRepository(this._api);
|
||||
|
@ -243,14 +243,15 @@ class AlbumService {
|
||||
int albumId, {
|
||||
List<Asset> add = const [],
|
||||
List<Asset> remove = const [],
|
||||
}) async {
|
||||
final album = await _albumRepository.getById(albumId);
|
||||
if (album == null) return;
|
||||
await _albumRepository.addAssets(album, add);
|
||||
await _albumRepository.removeAssets(album, remove);
|
||||
await _albumRepository.recalculateMetadata(album);
|
||||
await _albumRepository.update(album);
|
||||
}
|
||||
}) =>
|
||||
_albumRepository.transaction(() async {
|
||||
final album = await _albumRepository.get(albumId);
|
||||
if (album == null) return;
|
||||
await _albumRepository.addAssets(album, add);
|
||||
await _albumRepository.removeAssets(album, remove);
|
||||
await _albumRepository.recalculateMetadata(album);
|
||||
await _albumRepository.update(album);
|
||||
});
|
||||
|
||||
Future<bool> addAdditionalUserToAlbum(
|
||||
List<String> sharedUserIds,
|
||||
@ -285,20 +286,20 @@ class AlbumService {
|
||||
|
||||
Future<bool> deleteAlbum(Album album) async {
|
||||
try {
|
||||
final user = Store.get(StoreKey.currentUser);
|
||||
if (album.owner.value?.isarId == user.isarId) {
|
||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||
if (album.owner.value?.isarId == userId) {
|
||||
await _albumApiRepository.delete(album.remoteId!);
|
||||
}
|
||||
if (album.shared) {
|
||||
final foreignAssets =
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: user);
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
|
||||
await _albumRepository.delete(album.id);
|
||||
|
||||
final List<Album> albums = await _albumRepository.getAll(shared: true);
|
||||
final List<Asset> existing = [];
|
||||
for (Album album in albums) {
|
||||
existing.addAll(
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: user),
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]),
|
||||
);
|
||||
}
|
||||
final List<int> idsToRemove =
|
||||
@ -357,7 +358,7 @@ class AlbumService {
|
||||
|
||||
album.sharedUsers.remove(user);
|
||||
await _albumRepository.removeUsers(album, [user]);
|
||||
final a = await _albumRepository.getById(album.id);
|
||||
final a = await _albumRepository.get(album.id);
|
||||
// trigger watcher
|
||||
await _albumRepository.update(a!);
|
||||
|
||||
|
@ -1,27 +1,30 @@
|
||||
// ignore_for_file: null_argument_to_non_null_type
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@ -29,48 +32,54 @@ import 'package:openapi/api.dart';
|
||||
final assetServiceProvider = Provider(
|
||||
(ref) => AssetService(
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(exifInfoRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class AssetService {
|
||||
final IAssetApiRepository _assetApiRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IExifInfoRepository _exifInfoRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final IETagRepository _etagRepository;
|
||||
final IBackupRepository _backupRepository;
|
||||
final ApiService _apiService;
|
||||
final SyncService _syncService;
|
||||
final UserService _userService;
|
||||
final BackupService _backupService;
|
||||
final AlbumService _albumService;
|
||||
final log = Logger('AssetService');
|
||||
final Isar _db;
|
||||
|
||||
AssetService(
|
||||
this._assetApiRepository,
|
||||
this._assetRepository,
|
||||
this._exifInfoRepository,
|
||||
this._userRepository,
|
||||
this._etagRepository,
|
||||
this._backupRepository,
|
||||
this._apiService,
|
||||
this._syncService,
|
||||
this._userService,
|
||||
this._backupService,
|
||||
this._albumService,
|
||||
this._db,
|
||||
);
|
||||
|
||||
/// Checks the server for updated assets and updates the local database if
|
||||
/// required. Returns `true` if there were any changes.
|
||||
Future<bool> refreshRemoteAssets() async {
|
||||
final syncedUserIds = await _db.eTags.where().idProperty().findAll();
|
||||
final syncedUserIds = await _etagRepository.getAllIds();
|
||||
final List<User> syncedUsers = syncedUserIds.isEmpty
|
||||
? []
|
||||
: await _db.users
|
||||
.where()
|
||||
.anyOf(syncedUserIds, (q, id) => q.idEqualTo(id))
|
||||
.findAll();
|
||||
: await _userRepository.getByIds(syncedUserIds);
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
||||
users: syncedUsers,
|
||||
@ -175,7 +184,7 @@ class AssetService {
|
||||
/// Loads the exif information from the database. If there is none, loads
|
||||
/// the exif info from the server (remote assets only)
|
||||
Future<Asset> loadExif(Asset a) async {
|
||||
a.exifInfo ??= await _db.exifInfos.get(a.id);
|
||||
a.exifInfo ??= await _exifInfoRepository.get(a.id);
|
||||
// fileSize is always filled on the server but not set on client
|
||||
if (a.exifInfo?.fileSize == null) {
|
||||
if (a.isRemote) {
|
||||
@ -185,7 +194,7 @@ class AssetService {
|
||||
a.exifInfo = newExif;
|
||||
if (newExif != a.exifInfo) {
|
||||
if (a.isInDb) {
|
||||
_db.writeTxn(() => a.put(_db));
|
||||
_assetRepository.transaction(() => _assetRepository.update(a));
|
||||
} else {
|
||||
debugPrint("[loadExif] parameter Asset is not from DB!");
|
||||
}
|
||||
@ -214,7 +223,7 @@ class AssetService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Asset?>> changeFavoriteStatus(
|
||||
Future<List<Asset>> changeFavoriteStatus(
|
||||
List<Asset> assets,
|
||||
bool isFavorite,
|
||||
) async {
|
||||
@ -230,11 +239,11 @@ class AssetService {
|
||||
return assets;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error while changing favorite status", error, stack);
|
||||
return Future.value(null);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset?>> changeArchiveStatus(
|
||||
Future<List<Asset>> changeArchiveStatus(
|
||||
List<Asset> assets,
|
||||
bool isArchived,
|
||||
) async {
|
||||
@ -250,11 +259,11 @@ class AssetService {
|
||||
return assets;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error while changing archive status", error, stack);
|
||||
return Future.value(null);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset?>> changeDateTime(
|
||||
Future<List<Asset>?> changeDateTime(
|
||||
List<Asset> assets,
|
||||
String updatedDt,
|
||||
) async {
|
||||
@ -278,7 +287,7 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset?>> changeLocation(
|
||||
Future<List<Asset>?> changeLocation(
|
||||
List<Asset> assets,
|
||||
LatLng location,
|
||||
) async {
|
||||
@ -307,10 +316,10 @@ class AssetService {
|
||||
|
||||
Future<void> syncUploadedAssetToAlbums() async {
|
||||
try {
|
||||
final [selectedAlbums, excludedAlbums] = await Future.wait([
|
||||
_backupService.selectedAlbumsQuery().findAll(),
|
||||
_backupService.excludedAlbumsQuery().findAll(),
|
||||
]);
|
||||
final selectedAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
||||
final excludedAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
|
||||
final candidates = await _backupService.buildUploadCandidates(
|
||||
selectedAlbums,
|
||||
@ -319,12 +328,11 @@ class AssetService {
|
||||
);
|
||||
|
||||
await refreshRemoteAssets();
|
||||
final remoteAssets = await _db.assets
|
||||
.where()
|
||||
.localIdIsNotNull()
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.findAll();
|
||||
final owner = await _userRepository.me();
|
||||
final remoteAssets = await _assetRepository.getAll(
|
||||
ownerId: owner.isarId,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
|
||||
/// Map<AlbumName, [AssetId]>
|
||||
Map<String, List<String>> assetToAlbums = {};
|
||||
|
@ -9,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/main.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
@ -18,6 +19,8 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
@ -38,7 +41,6 @@ import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
|
||||
@ -357,7 +359,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<bool> _onAssetsChanged() async {
|
||||
final Isar db = await loadDb();
|
||||
final db = await loadDb();
|
||||
|
||||
HttpOverrides.global = HttpSSLCertOverride();
|
||||
ApiService apiService = ApiService();
|
||||
@ -366,7 +368,9 @@ class BackgroundService {
|
||||
AppSettingsService settingsService = AppSettingsService();
|
||||
AlbumRepository albumRepository = AlbumRepository(db);
|
||||
AssetRepository assetRepository = AssetRepository(db);
|
||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||
BackupRepository backupRepository = BackupRepository(db);
|
||||
ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
|
||||
ETagRepository eTagRepository = ETagRepository(db);
|
||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
|
||||
@ -382,11 +386,15 @@ class BackgroundService {
|
||||
EntityService entityService =
|
||||
EntityService(assetRepository, userRepository);
|
||||
SyncService syncSerive = SyncService(
|
||||
db,
|
||||
hashService,
|
||||
entityService,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
exifInfoRepository,
|
||||
userRepository,
|
||||
eTagRepository,
|
||||
);
|
||||
UserService userService = UserService(
|
||||
partnerApiRepository,
|
||||
@ -400,22 +408,24 @@ class BackgroundService {
|
||||
entityService,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
backupAlbumRepository,
|
||||
backupRepository,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
);
|
||||
BackupService backupService = BackupService(
|
||||
apiService,
|
||||
db,
|
||||
settingService,
|
||||
albumService,
|
||||
albumMediaRepository,
|
||||
fileMediaRepository,
|
||||
assetRepository,
|
||||
assetMediaRepository,
|
||||
);
|
||||
|
||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
||||
final selectedAlbums =
|
||||
await backupRepository.getAllBySelection(BackupSelection.select);
|
||||
final excludedAlbums =
|
||||
await backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
if (selectedAlbums.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
@ -433,28 +443,28 @@ class BackgroundService {
|
||||
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),
|
||||
);
|
||||
db.backupAlbums.deleteAllSync(toDelete);
|
||||
db.backupAlbums.putAllSync(toUpsert);
|
||||
});
|
||||
|
||||
final dbAlbums =
|
||||
await backupRepository.getAll(sort: BackupAlbumSort.id);
|
||||
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),
|
||||
);
|
||||
await backupRepository.deleteAll(toDelete);
|
||||
await backupRepository.updateAll(toUpsert);
|
||||
} else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
|
||||
Store.put(StoreKey.backupFailedSince, DateTime.now());
|
||||
return false;
|
||||
|
@ -9,9 +9,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
@ -20,14 +20,13 @@ import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -37,11 +36,11 @@ import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
final backupServiceProvider = Provider(
|
||||
(ref) => BackupService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
@ -49,21 +48,21 @@ final backupServiceProvider = Provider(
|
||||
class BackupService {
|
||||
final httpClient = http.Client();
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final Logger _log = Logger("BackupService");
|
||||
final AppSettingsService _appSetting;
|
||||
final AlbumService _albumService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IAssetMediaRepository _assetMediaRepository;
|
||||
|
||||
BackupService(
|
||||
this._apiService,
|
||||
this._db,
|
||||
this._appSetting,
|
||||
this._albumService,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
this._assetRepository,
|
||||
this._assetMediaRepository,
|
||||
);
|
||||
|
||||
@ -78,24 +77,17 @@ class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) {
|
||||
final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList();
|
||||
return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates));
|
||||
}
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) =>
|
||||
_assetRepository.transaction(
|
||||
() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds),
|
||||
);
|
||||
|
||||
/// 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();
|
||||
final duplicates = await _assetRepository.getAllDuplicatedAssetIds();
|
||||
return duplicates.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
|
||||
/// if `useTimeFilter` is set to true, all assets will be returned
|
||||
Future<Set<BackupCandidate>> buildUploadCandidates(
|
||||
|
@ -34,19 +34,19 @@ class BackupVerificationService {
|
||||
final owner = Store.get(StoreKey.currentUser).isarId;
|
||||
final List<Asset> onlyLocal = await _assetRepository.getAll(
|
||||
ownerId: owner,
|
||||
remote: false,
|
||||
state: AssetState.local,
|
||||
limit: limit,
|
||||
);
|
||||
final List<Asset> remoteMatches = await _assetRepository.getMatches(
|
||||
assets: onlyLocal,
|
||||
ownerId: owner,
|
||||
remote: true,
|
||||
state: AssetState.remote,
|
||||
limit: limit,
|
||||
);
|
||||
final List<Asset> localMatches = await _assetRepository.getMatches(
|
||||
assets: remoteMatches,
|
||||
ownerId: owner,
|
||||
remote: false,
|
||||
state: AssetState.local,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
|
@ -130,7 +130,9 @@ class HashService {
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
await _assetRepository.upsertDeviceAssets(validHashes);
|
||||
|
||||
await _assetRepository
|
||||
.transaction(() => _assetRepository.upsertDeviceAssets(validHashes));
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,8 @@ class StackService {
|
||||
|
||||
removeAssets.add(asset);
|
||||
}
|
||||
await _assetRepository.updateAll(removeAssets);
|
||||
await _assetRepository
|
||||
.transaction(() => _assetRepository.updateAll(removeAssets));
|
||||
} catch (error) {
|
||||
debugPrint("Error while deleting stack: $error");
|
||||
}
|
||||
|
@ -5,48 +5,66 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(hashServiceProvider),
|
||||
ref.watch(entityServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(albumApiRepositoryProvider),
|
||||
ref.watch(albumRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(exifInfoRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final EntityService _entityService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IAlbumApiRepository _albumApiRepository;
|
||||
final IAlbumRepository _albumRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IExifInfoRepository _exifInfoRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final IETagRepository _eTagRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(
|
||||
this._db,
|
||||
this._hashService,
|
||||
this._entityService,
|
||||
this._albumMediaRepository,
|
||||
this._albumApiRepository,
|
||||
this._albumRepository,
|
||||
this._assetRepository,
|
||||
this._exifInfoRepository,
|
||||
this._userRepository,
|
||||
this._eTagRepository,
|
||||
);
|
||||
|
||||
// public methods:
|
||||
@ -119,7 +137,7 @@ class SyncService {
|
||||
/// Returns `true`if there were any changes
|
||||
Future<bool> _syncUsersFromServer(List<User> users) async {
|
||||
users.sortBy((u) => u.id);
|
||||
final dbUsers = await _db.users.where().sortById().findAll();
|
||||
final dbUsers = await _userRepository.getAll(sortBy: UserSort.id);
|
||||
assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!");
|
||||
final List<int> toDelete = [];
|
||||
final List<User> toUpsert = [];
|
||||
@ -141,9 +159,9 @@ class SyncService {
|
||||
onlySecond: (User b) => toDelete.add(b.isarId),
|
||||
);
|
||||
if (changes) {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.users.deleteAll(toDelete);
|
||||
await _db.users.putAll(toUpsert);
|
||||
await _userRepository.transaction(() async {
|
||||
await _userRepository.deleteById(toDelete);
|
||||
await _userRepository.upsertAll(toUpsert);
|
||||
});
|
||||
}
|
||||
return changes;
|
||||
@ -152,15 +170,15 @@ class SyncService {
|
||||
/// Syncs a new asset to the db. Returns `true` if successful
|
||||
Future<bool> _syncNewAssetToDb(Asset a) async {
|
||||
final Asset? inDb =
|
||||
await _db.assets.getByOwnerIdChecksum(a.ownerId, a.checksum);
|
||||
await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum);
|
||||
if (inDb != null) {
|
||||
// unify local/remote assets by replacing the
|
||||
// local-only asset in the DB with a local&remote asset
|
||||
a = inDb.updatedCopy(a);
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() => a.put(_db));
|
||||
} on IsarError catch (e) {
|
||||
await _assetRepository.update(a);
|
||||
} catch (e) {
|
||||
_log.severe("Failed to put new asset into db", e);
|
||||
return false;
|
||||
}
|
||||
@ -175,9 +193,9 @@ class SyncService {
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
) async {
|
||||
final currentUser = Store.get(StoreKey.currentUser);
|
||||
final currentUser = await _userRepository.me();
|
||||
final DateTime? since =
|
||||
_db.eTags.getSync(currentUser.isarId)?.time?.toUtc();
|
||||
(await _eTagRepository.get(currentUser.isarId))?.time?.toUtc();
|
||||
if (since == null) return null;
|
||||
final DateTime now = DateTime.now();
|
||||
final (toUpsert, toDelete) = await getChangedAssets(users, since);
|
||||
@ -198,7 +216,7 @@ class SyncService {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
return null;
|
||||
@ -206,23 +224,21 @@ class SyncService {
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
return _db.writeTxn(() async {
|
||||
final idsToRemove = await _db.assets
|
||||
.remote(idsToDelete)
|
||||
.filter()
|
||||
.localIdIsNull()
|
||||
.idProperty()
|
||||
.findAll();
|
||||
await _db.assets.deleteAll(idsToRemove);
|
||||
await _db.exifInfos.deleteAll(idsToRemove);
|
||||
final onlyLocal = await _db.assets.remote(idsToDelete).findAll();
|
||||
if (onlyLocal.isNotEmpty) {
|
||||
for (final Asset a in onlyLocal) {
|
||||
a.remoteId = null;
|
||||
a.isTrashed = false;
|
||||
}
|
||||
await _db.assets.putAll(onlyLocal);
|
||||
return _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
idsToDelete,
|
||||
state: AssetState.remote,
|
||||
);
|
||||
final merged = await _assetRepository.getAllByRemoteId(
|
||||
idsToDelete,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
if (merged.isEmpty) return;
|
||||
for (final Asset asset in merged) {
|
||||
asset.remoteId = null;
|
||||
asset.isTrashed = false;
|
||||
}
|
||||
await _assetRepository.updateAll(merged);
|
||||
});
|
||||
}
|
||||
|
||||
@ -237,12 +253,7 @@ class SyncService {
|
||||
return false;
|
||||
}
|
||||
await _syncUsersFromServer(serverUsers);
|
||||
final List<User> users = await _db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.or()
|
||||
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.findAll();
|
||||
final List<User> users = await _userRepository.getAllAccessible();
|
||||
bool changes = false;
|
||||
for (User u in users) {
|
||||
changes |= await _syncRemoteAssetsForUser(u, loadAssets);
|
||||
@ -259,11 +270,10 @@ class SyncService {
|
||||
if (remote == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.where()
|
||||
.ownerIdEqualToAnyChecksum(user.isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
final List<Asset> inDb = await _assetRepository.getAll(
|
||||
ownerId: user.isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
);
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
|
||||
remote.sort(Asset.compareByChecksum);
|
||||
@ -278,9 +288,9 @@ class SyncService {
|
||||
}
|
||||
final idsToDelete = toRemove.map((e) => e.id).toList();
|
||||
try {
|
||||
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
|
||||
await _assetRepository.deleteById(idsToDelete);
|
||||
await upsertAssetsWithExif(toAdd + toUpdate);
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
await _updateUserAssetsETag([user], now);
|
||||
@ -289,12 +299,12 @@ class SyncService {
|
||||
|
||||
Future<void> _updateUserAssetsETag(List<User> users, DateTime time) {
|
||||
final etags = users.map((u) => ETag(id: u.id, time: time)).toList();
|
||||
return _db.writeTxn(() => _db.eTags.putAll(etags));
|
||||
return _eTagRepository.upsertAll(etags);
|
||||
}
|
||||
|
||||
Future<void> _clearUserAssetsETag(List<User> users) {
|
||||
final ids = users.map((u) => u.id).toList();
|
||||
return _db.writeTxn(() => _db.eTags.deleteAllById(ids));
|
||||
return _eTagRepository.deleteByIds(ids);
|
||||
}
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
@ -305,15 +315,13 @@ class SyncService {
|
||||
) async {
|
||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||
|
||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||
if (isShared) {
|
||||
query = baseQuery.sharedEqualTo(true);
|
||||
} else {
|
||||
final User me = Store.get(StoreKey.currentUser);
|
||||
query = baseQuery.owner((q) => q.isarIdEqualTo(me.isarId));
|
||||
}
|
||||
final List<Album> dbAlbums = await query.sortByRemoteId().findAll();
|
||||
final User me = await _userRepository.me();
|
||||
final List<Album> dbAlbums = await _albumRepository.getAll(
|
||||
remote: true,
|
||||
shared: isShared ? true : null,
|
||||
ownerId: isShared ? null : me.isarId,
|
||||
sortBy: AlbumSort.remoteId,
|
||||
);
|
||||
assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!");
|
||||
|
||||
final List<Asset> toDelete = [];
|
||||
@ -333,10 +341,7 @@ class SyncService {
|
||||
if (isShared && toDelete.isNotEmpty) {
|
||||
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
||||
if (idsToRemove.isNotEmpty) {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(idsToRemove);
|
||||
await _db.exifInfos.deleteAll(idsToRemove);
|
||||
});
|
||||
await _assetRepository.deleteById(idsToRemove);
|
||||
}
|
||||
} else {
|
||||
assert(toDelete.isEmpty);
|
||||
@ -360,8 +365,11 @@ class SyncService {
|
||||
// i.e. it will always be null. Save it here.
|
||||
final originalDto = dto;
|
||||
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||
final assetsInDb =
|
||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||
|
||||
final assetsInDb = await _assetRepository.getByAlbum(
|
||||
album,
|
||||
sortBy: AssetSort.ownerIdChecksum,
|
||||
);
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
@ -391,7 +399,7 @@ class SyncService {
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
||||
await upsertAssetsWithExif(updated);
|
||||
final assetsToLink = existingInDb + updated;
|
||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||
final usersToLink = await _userRepository.getByIds(userIdsToAdd);
|
||||
|
||||
album.name = dto.name;
|
||||
album.shared = dto.shared;
|
||||
@ -402,32 +410,33 @@ class SyncService {
|
||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||
album.shared = dto.shared;
|
||||
album.activityEnabled = dto.activityEnabled;
|
||||
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
|
||||
album.thumbnail.value = await _db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
|
||||
.findFirst();
|
||||
final remoteThumbnailAssetId = dto.remoteThumbnailAssetId;
|
||||
if (remoteThumbnailAssetId != null &&
|
||||
album.thumbnail.value?.remoteId != remoteThumbnailAssetId) {
|
||||
album.thumbnail.value =
|
||||
await _assetRepository.getByRemoteId(remoteThumbnailAssetId);
|
||||
}
|
||||
|
||||
// write & commit all changes to DB
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await album.thumbnail.save();
|
||||
await album.sharedUsers
|
||||
.update(link: usersToLink, unlink: usersToUnlink);
|
||||
await album.assets.update(link: assetsToLink, unlink: toUnlink.cast());
|
||||
await _db.albums.put(album);
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
await _albumRepository.addUsers(album, usersToLink);
|
||||
await _albumRepository.removeUsers(album, usersToUnlink);
|
||||
await _albumRepository.addAssets(album, assetsToLink);
|
||||
await _albumRepository.removeAssets(album, toUnlink);
|
||||
await _albumRepository.recalculateMetadata(album);
|
||||
await _albumRepository.update(album);
|
||||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe("Failed to sync remote album to database", e);
|
||||
}
|
||||
|
||||
if (album.shared || dto.shared) {
|
||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||
final userId = (await _userRepository.me()).isarId;
|
||||
final foreign =
|
||||
await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
|
||||
existing.addAll(foreign);
|
||||
|
||||
// delete assets in DB unless they belong to this user or part of some other shared album
|
||||
@ -456,7 +465,7 @@ class SyncService {
|
||||
await upsertAssetsWithExif(updated);
|
||||
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
await _albumRepository.create(album);
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||
@ -474,27 +483,18 @@ class SyncService {
|
||||
_log.info("Removing local album $album from DB");
|
||||
// delete assets in DB unless they are remote or part of some other album
|
||||
deleteCandidates.addAll(
|
||||
await album.assets.filter().remoteIdIsNull().findAll(),
|
||||
await _assetRepository.getByAlbum(album, state: AssetState.local),
|
||||
);
|
||||
} else if (album.shared) {
|
||||
final User user = Store.get(StoreKey.currentUser);
|
||||
// delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
|
||||
final userIds = await _db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.isarIdProperty()
|
||||
.findAll();
|
||||
userIds.add(user.isarId);
|
||||
final orphanedAssets = await album.assets
|
||||
.filter()
|
||||
.not()
|
||||
.anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id))
|
||||
.findAll();
|
||||
final userIds =
|
||||
(await _userRepository.getAllAccessible()).map((user) => user.isarId);
|
||||
final orphanedAssets =
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: userIds);
|
||||
deleteCandidates.addAll(orphanedAssets);
|
||||
}
|
||||
try {
|
||||
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));
|
||||
assert(ok);
|
||||
await _albumRepository.delete(album.id);
|
||||
_log.info("Removed local album $album from DB");
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove local album $album from DB", e);
|
||||
@ -509,7 +509,7 @@ class SyncService {
|
||||
]) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
final inDb =
|
||||
await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();
|
||||
await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId);
|
||||
final List<Asset> deleteCandidates = [];
|
||||
final List<Asset> existing = [];
|
||||
assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
|
||||
@ -536,10 +536,9 @@ class SyncService {
|
||||
"${toDelete.length} assets to delete, ${toUpdate.length} to update",
|
||||
);
|
||||
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.exifInfos.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteById(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
});
|
||||
_log.info(
|
||||
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
|
||||
@ -570,13 +569,13 @@ class SyncService {
|
||||
await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||
final inDb = await dbAlbum.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
final inDb = await _assetRepository.getByAlbum(
|
||||
dbAlbum,
|
||||
ownerId: (await _userRepository.me()).isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
);
|
||||
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
@ -597,15 +596,14 @@ class SyncService {
|
||||
"Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
|
||||
);
|
||||
if (assetCountOnDevice !=
|
||||
_db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) {
|
||||
await _db.writeTxn(
|
||||
() => _db.eTags.put(
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount) {
|
||||
await _eTagRepository.upsertAll([
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
);
|
||||
]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -625,23 +623,21 @@ class SyncService {
|
||||
dbAlbum.thumbnail.value = null;
|
||||
}
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await dbAlbum.assets
|
||||
.update(link: existingInDb + updated, unlink: toDelete);
|
||||
await _db.albums.put(dbAlbum);
|
||||
dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst();
|
||||
await dbAlbum.thumbnail.save();
|
||||
await _db.eTags.put(
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(updated + toUpdate);
|
||||
await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
|
||||
await _albumRepository.removeAssets(dbAlbum, toDelete);
|
||||
await _albumRepository.recalculateMetadata(dbAlbum);
|
||||
await _albumRepository.update(dbAlbum);
|
||||
await _eTagRepository.upsertAll([
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
);
|
||||
]);
|
||||
});
|
||||
_log.info("Synced changes of local album ${deviceAlbum.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e);
|
||||
}
|
||||
|
||||
@ -657,7 +653,8 @@ class SyncService {
|
||||
final int totalOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final int lastKnownTotal =
|
||||
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ??
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount ??
|
||||
0;
|
||||
if (totalOnDevice <= lastKnownTotal) {
|
||||
return false;
|
||||
@ -675,16 +672,17 @@ class SyncService {
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await dbAlbum.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(dbAlbum);
|
||||
await _db.eTags.put(
|
||||
ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice),
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(updated);
|
||||
await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
|
||||
await _albumRepository.recalculateMetadata(dbAlbum);
|
||||
await _albumRepository.update(dbAlbum);
|
||||
await _eTagRepository.upsertAll(
|
||||
[ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)],
|
||||
);
|
||||
});
|
||||
_log.info("Fast synced local album ${deviceAlbum.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe(
|
||||
"Failed to fast sync local album ${deviceAlbum.name} to DB",
|
||||
e,
|
||||
@ -719,9 +717,9 @@ class SyncService {
|
||||
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
||||
album.thumbnail.value = thumb;
|
||||
try {
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
await _albumRepository.create(album);
|
||||
_log.info("Added a new local album to DB: ${album.name}");
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe("Failed to add new local album ${album.name} to DB", e);
|
||||
}
|
||||
}
|
||||
@ -732,7 +730,7 @@ class SyncService {
|
||||
) async {
|
||||
if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
|
||||
|
||||
final List<Asset?> inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
final List<Asset?> inDb = await _assetRepository.getAllByOwnerIdChecksum(
|
||||
assets.map((a) => a.ownerId).toInt64List(),
|
||||
assets.map((a) => a.checksum).toList(growable: false),
|
||||
);
|
||||
@ -746,7 +744,7 @@ class SyncService {
|
||||
}
|
||||
if (b.canUpdate(assets[i])) {
|
||||
final updated = b.updatedCopy(assets[i]);
|
||||
assert(updated.id != Isar.autoIncrement);
|
||||
assert(updated.isInDb);
|
||||
toUpsert.add(updated);
|
||||
} else {
|
||||
existing.add(b);
|
||||
@ -758,24 +756,22 @@ class SyncService {
|
||||
|
||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||
if (assets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final exifInfos = assets.map((e) => e.exifInfo).whereNotNull().toList();
|
||||
if (assets.isEmpty) return;
|
||||
final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList();
|
||||
try {
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(assets);
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(assets);
|
||||
for (final Asset added in assets) {
|
||||
added.exifInfo?.id = added.id;
|
||||
}
|
||||
await _db.exifInfos.putAll(exifInfos);
|
||||
await _exifInfoRepository.updateAll(exifInfos);
|
||||
});
|
||||
_log.info("Upserted ${assets.length} assets into the DB");
|
||||
} on IsarError catch (e) {
|
||||
} catch (e) {
|
||||
_log.severe("Failed to upsert ${assets.length} assets into the DB", e);
|
||||
// give details on the errors
|
||||
assets.sort(Asset.compareByOwnerChecksum);
|
||||
final inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
final inDb = await _assetRepository.getAllByOwnerIdChecksum(
|
||||
assets.map((e) => e.ownerId).toInt64List(),
|
||||
assets.map((e) => e.checksum).toList(growable: false),
|
||||
);
|
||||
@ -783,7 +779,7 @@ class SyncService {
|
||||
final Asset a = assets[i];
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
if (a.id != Isar.autoIncrement) {
|
||||
if (!a.isInDb) {
|
||||
_log.warning(
|
||||
"Trying to update an asset that does not exist in DB:\n$a",
|
||||
);
|
||||
@ -827,19 +823,19 @@ class SyncService {
|
||||
return deviceAlbum.name != dbAlbum.name ||
|
||||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
||||
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount;
|
||||
}
|
||||
|
||||
Future<bool> _removeAllLocalAlbumsAndAssets() async {
|
||||
try {
|
||||
final assets = await _db.assets.where().localIdIsNotNull().findAll();
|
||||
final assets = await _assetRepository.getAllLocal();
|
||||
final (toDelete, toUpdate) =
|
||||
_handleAssetRemoval(assets, [], remote: false);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await _db.albums.where().localIdIsNotNull().deleteAll();
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteById(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
await _albumRepository.deleteAllLocal();
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
@ -1,17 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../repository.mocks.dart';
|
||||
import '../../service.mocks.dart';
|
||||
import '../../test_utils.dart';
|
||||
|
||||
void main() {
|
||||
int assetIdCounter = 0;
|
||||
Asset makeAsset({
|
||||
required String checksum,
|
||||
String? localId,
|
||||
@ -20,6 +24,7 @@ void main() {
|
||||
}) {
|
||||
final DateTime date = DateTime(2000);
|
||||
return Asset(
|
||||
id: assetIdCounter++,
|
||||
checksum: checksum,
|
||||
localId: localId,
|
||||
remoteId: remoteId,
|
||||
@ -37,9 +42,13 @@ void main() {
|
||||
}
|
||||
|
||||
group('Test SyncService grouped', () {
|
||||
late final Isar db;
|
||||
final MockHashService hs = MockHashService();
|
||||
final MockEntityService entityService = MockEntityService();
|
||||
final MockAlbumRepository albumRepository = MockAlbumRepository();
|
||||
final MockAssetRepository assetRepository = MockAssetRepository();
|
||||
final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository();
|
||||
final MockUserRepository userRepository = MockUserRepository();
|
||||
final MockETagRepository eTagRepository = MockETagRepository();
|
||||
final MockAlbumMediaRepository albumMediaRepository =
|
||||
MockAlbumMediaRepository();
|
||||
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
||||
@ -53,7 +62,7 @@ void main() {
|
||||
late SyncService s;
|
||||
setUpAll(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
db = await TestUtils.initIsar();
|
||||
final db = await TestUtils.initIsar();
|
||||
ImmichLogger();
|
||||
db.writeTxnSync(() => db.clearSync());
|
||||
Store.init(db);
|
||||
@ -67,16 +76,43 @@ void main() {
|
||||
makeAsset(checksum: "e", localId: "3"),
|
||||
];
|
||||
setUp(() {
|
||||
db.writeTxnSync(() {
|
||||
db.assets.clearSync();
|
||||
db.assets.putAllSync(initialAssets);
|
||||
});
|
||||
s = SyncService(
|
||||
db,
|
||||
hs,
|
||||
entityService,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
exifInfoRepository,
|
||||
userRepository,
|
||||
eTagRepository,
|
||||
);
|
||||
when(() => eTagRepository.get(owner.isarId))
|
||||
.thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now()));
|
||||
when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {});
|
||||
when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {});
|
||||
when(() => userRepository.me()).thenAnswer((_) async => owner);
|
||||
when(() => userRepository.getAll(sortBy: UserSort.id))
|
||||
.thenAnswer((_) async => [owner]);
|
||||
when(() => userRepository.getAllAccessible())
|
||||
.thenAnswer((_) async => [owner]);
|
||||
when(
|
||||
() => assetRepository.getAll(
|
||||
ownerId: owner.isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
),
|
||||
).thenAnswer((_) async => initialAssets);
|
||||
when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
|
||||
.thenAnswer((_) async => [initialAssets[3], null, null]);
|
||||
when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
|
||||
when(() => assetRepository.deleteById(any())).thenAnswer((_) async {});
|
||||
when(() => exifInfoRepository.updateAll(any()))
|
||||
.thenAnswer((_) async => []);
|
||||
when(() => assetRepository.transaction<void>(any())).thenAnswer(
|
||||
(call) => (call.positionalArguments.first as Function).call(),
|
||||
);
|
||||
when(() => assetRepository.transaction<Null>(any())).thenAnswer(
|
||||
(call) => (call.positionalArguments.first as Function).call(),
|
||||
);
|
||||
});
|
||||
test('test inserting existing assets', () async {
|
||||
@ -85,7 +121,6 @@ void main() {
|
||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
@ -93,7 +128,7 @@ void main() {
|
||||
refreshUsers: () => [owner],
|
||||
);
|
||||
expect(c1, isFalse);
|
||||
expect(db.assets.countSync(), 5);
|
||||
verifyNever(() => assetRepository.updateAll(any()));
|
||||
});
|
||||
|
||||
test('test inserting new assets', () async {
|
||||
@ -105,7 +140,6 @@ void main() {
|
||||
makeAsset(checksum: "f", remoteId: "1-4"),
|
||||
makeAsset(checksum: "g", remoteId: "3-1"),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
@ -113,7 +147,11 @@ void main() {
|
||||
refreshUsers: () => [owner],
|
||||
);
|
||||
expect(c1, isTrue);
|
||||
expect(db.assets.countSync(), 7);
|
||||
final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]);
|
||||
verify(
|
||||
() => assetRepository
|
||||
.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]),
|
||||
);
|
||||
});
|
||||
|
||||
test('test syncing duplicate assets', () async {
|
||||
@ -125,7 +163,6 @@ void main() {
|
||||
makeAsset(checksum: "i", remoteId: "2-1c"),
|
||||
makeAsset(checksum: "j", remoteId: "2-1d"),
|
||||
];
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
@ -133,7 +170,12 @@ void main() {
|
||||
refreshUsers: () => [owner],
|
||||
);
|
||||
expect(c1, isTrue);
|
||||
expect(db.assets.countSync(), 8);
|
||||
when(
|
||||
() => assetRepository.getAll(
|
||||
ownerId: owner.isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
),
|
||||
).thenAnswer((_) async => remoteAssets);
|
||||
final bool c2 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
@ -141,7 +183,13 @@ void main() {
|
||||
refreshUsers: () => [owner],
|
||||
);
|
||||
expect(c2, isFalse);
|
||||
expect(db.assets.countSync(), 8);
|
||||
final currentState = [...remoteAssets];
|
||||
when(
|
||||
() => assetRepository.getAll(
|
||||
ownerId: owner.isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
),
|
||||
).thenAnswer((_) async => currentState);
|
||||
remoteAssets.removeAt(4);
|
||||
final bool c3 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
@ -150,7 +198,6 @@ void main() {
|
||||
refreshUsers: () => [owner],
|
||||
);
|
||||
expect(c3, isTrue);
|
||||
expect(db.assets.countSync(), 7);
|
||||
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
|
||||
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
|
||||
final bool c4 = await s.syncRemoteAssetsToDb(
|
||||
@ -160,10 +207,21 @@ void main() {
|
||||
refreshUsers: () => [owner],
|
||||
);
|
||||
expect(c4, isTrue);
|
||||
expect(db.assets.countSync(), 9);
|
||||
});
|
||||
|
||||
test('test efficient sync', () async {
|
||||
when(
|
||||
() => assetRepository.deleteAllByRemoteId(
|
||||
[initialAssets[1].remoteId!, initialAssets[2].remoteId!],
|
||||
state: AssetState.remote,
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => assetRepository
|
||||
.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged),
|
||||
).thenAnswer((_) async => [initialAssets[2]]);
|
||||
when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
|
||||
.thenAnswer((_) async => [initialAssets[0], null, null]); //afg
|
||||
final List<Asset> toUpsert = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"), // changed
|
||||
makeAsset(checksum: "f", remoteId: "0-2"), // new
|
||||
@ -171,6 +229,8 @@ void main() {
|
||||
];
|
||||
toUpsert[0].isFavorite = true;
|
||||
final List<String> toDelete = ["2-1", "1-1"];
|
||||
final expected = [...toUpsert];
|
||||
expected[0].id = initialAssets[0].id;
|
||||
final bool c = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: (user, since) async => (toUpsert, toDelete),
|
||||
@ -178,7 +238,7 @@ void main() {
|
||||
refreshUsers: () => throw Exception(),
|
||||
);
|
||||
expect(c, isTrue);
|
||||
expect(db.assets.countSync(), 6);
|
||||
verify(() => assetRepository.updateAll(expected));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
@ -16,6 +18,10 @@ class MockUserRepository extends Mock implements IUserRepository {}
|
||||
|
||||
class MockBackupRepository extends Mock implements IBackupRepository {}
|
||||
|
||||
class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
|
||||
|
||||
class MockETagRepository extends Mock implements IETagRepository {}
|
||||
|
||||
class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
|
||||
|
||||
class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
|
||||
|
@ -29,6 +29,13 @@ void main() {
|
||||
albumMediaRepository = MockAlbumMediaRepository();
|
||||
albumApiRepository = MockAlbumApiRepository();
|
||||
|
||||
when(() => albumRepository.transaction<void>(any())).thenAnswer(
|
||||
(call) => (call.positionalArguments.first as Function).call(),
|
||||
);
|
||||
when(() => assetRepository.transaction<Null>(any())).thenAnswer(
|
||||
(call) => (call.positionalArguments.first as Function).call(),
|
||||
);
|
||||
|
||||
sut = AlbumService(
|
||||
userService,
|
||||
syncService,
|
||||
@ -144,7 +151,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
when(
|
||||
() => albumRepository.getById(AlbumStub.oneAsset.id),
|
||||
() => albumRepository.get(AlbumStub.oneAsset.id),
|
||||
).thenAnswer((_) async => AlbumStub.oneAsset);
|
||||
when(
|
||||
() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2]),
|
||||
|
Loading…
x
Reference in New Issue
Block a user