diff --git a/mobile/lib/interfaces/album.interface.dart b/mobile/lib/interfaces/album.interface.dart index 86174b7dab..2d8c460b67 100644 --- a/mobile/lib/interfaces/album.interface.dart +++ b/mobile/lib/interfaces/album.interface.dart @@ -51,6 +51,8 @@ abstract interface class IAlbumRepository implements IDatabaseRepository { Stream watchAlbum(int id); Stream getRenderListStream(Album album); + + Future clearTable(); } enum AlbumSort { remoteId, localId } diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart index 65cca6e86c..4096a55061 100644 --- a/mobile/lib/interfaces/asset.interface.dart +++ b/mobile/lib/interfaces/asset.interface.dart @@ -41,7 +41,7 @@ abstract interface class IAssetRepository implements IDatabaseRepository { Future deleteAllByRemoteId(List ids, {AssetState? state}); - Future deleteById(List ids); + Future deleteByIds(List ids); Future> getMatches({ required List assets, @@ -59,6 +59,10 @@ abstract interface class IAssetRepository implements IDatabaseRepository { Future> getAllDuplicatedAssetIds(); Future> getStackAssets(String stackId); + + Future clearTable(); + + Stream watchAsset(int id, {bool fireImmediately = false}); } enum AssetSort { checksum, ownerIdChecksum } diff --git a/mobile/lib/interfaces/etag.interface.dart b/mobile/lib/interfaces/etag.interface.dart index e567235d1b..22942b0e34 100644 --- a/mobile/lib/interfaces/etag.interface.dart +++ b/mobile/lib/interfaces/etag.interface.dart @@ -11,4 +11,6 @@ abstract interface class IETagRepository implements IDatabaseRepository { Future upsertAll(List etags); Future deleteByIds(List ids); + + Future clearTable(); } diff --git a/mobile/lib/interfaces/exif_info.interface.dart b/mobile/lib/interfaces/exif_info.interface.dart index 86608c26d0..ce379c926c 100644 --- a/mobile/lib/interfaces/exif_info.interface.dart +++ b/mobile/lib/interfaces/exif_info.interface.dart @@ -9,4 +9,6 @@ abstract interface class IExifInfoRepository implements IDatabaseRepository { Future> updateAll(List exifInfos); Future delete(int id); + + Future clearTable(); } diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart index e6175a7dc9..601730f3f8 100644 --- a/mobile/lib/interfaces/user.interface.dart +++ b/mobile/lib/interfaces/user.interface.dart @@ -18,6 +18,8 @@ abstract interface class IUserRepository implements IDatabaseRepository { Future deleteById(List ids); Future me(); + + Future clearTable(); } enum UserSort { id } diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index 9252de01bf..bfc03cdd00 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -2,28 +2,40 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/asset.service.dart'; +import 'package:immich_mobile/services/etag.service.dart'; +import 'package:immich_mobile/services/exif.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; -import 'package:immich_mobile/utils/db.dart'; import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; +final assetProvider = StateNotifierProvider((ref) { + return AssetNotifier( + ref.watch(assetServiceProvider), + ref.watch(albumServiceProvider), + ref.watch(userServiceProvider), + ref.watch(syncServiceProvider), + ref.watch(etagServiceProvider), + ref.watch(exifServiceProvider), + ref, + ); +}); + class AssetNotifier extends StateNotifier { final AssetService _assetService; final AlbumService _albumService; final UserService _userService; final SyncService _syncService; - final Isar _db; + final ETagService _etagService; + final ExifService _exifService; final StateNotifierProviderRef _ref; final log = Logger('AssetNotifier'); bool _getAllAssetInProgress = false; @@ -34,7 +46,8 @@ class AssetNotifier extends StateNotifier { this._albumService, this._userService, this._syncService, - this._db, + this._etagService, + this._exifService, this._ref, ) : super(false); @@ -48,7 +61,7 @@ class AssetNotifier extends StateNotifier { _getAllAssetInProgress = true; state = true; if (clear) { - await clearAssetsAndAlbums(_db); + await clearAllAssets(); log.info("Manual refresh requested, cleared assets and albums from db"); } final bool changedUsers = await _userService.refreshUsers(); @@ -68,8 +81,15 @@ class AssetNotifier extends StateNotifier { } } - Future clearAllAsset() { - return clearAssetsAndAlbums(_db); + Future clearAllAssets() async { + await Store.delete(StoreKey.assetETag); + await Future.wait([ + _assetService.clearTable(), + _exifService.clearTable(), + _albumService.clearTable(), + _userService.clearTable(), + _etagService.clearTable(), + ]); } Future onNewAssetUploaded(Asset newAsset) async { @@ -78,102 +98,43 @@ class AssetNotifier extends StateNotifier { await _syncService.syncNewAssetToDb(newAsset); } - Future deleteLocalOnlyAssets( - Iterable deleteAssets, { - bool onlyBackedUp = false, - }) async { + Future deleteLocalAssets(List assets) async { _deleteInProgress = true; state = true; try { - // Filter the assets based on the backed-up status - final assets = onlyBackedUp - ? deleteAssets.where((e) => e.storage == AssetState.merged) - : deleteAssets; - - if (assets.isEmpty) { - return false; // No assets to delete - } - - // Proceed with local deletion of the filtered assets - final localDeleted = await _deleteLocalAssets(assets); - - if (localDeleted.isNotEmpty) { - final localOnlyIds = assets - .where((e) => e.storage == AssetState.local) - .map((e) => e.id) - .toList(); - - // Update merged assets to remote-only - final mergedAssets = - assets.where((e) => e.storage == AssetState.merged).map((e) { - e.localId = null; - return e; - }).toList(); - - // Update the local database - await _db.writeTxn(() async { - if (mergedAssets.isNotEmpty) { - await _db.assets - .putAll(mergedAssets); // Use the filtered merged assets - } - await _db.exifInfos.deleteAll(localOnlyIds); - await _db.assets.deleteAll(localOnlyIds); - }); - - return true; - } + await _assetService.deleteLocalAssets(assets); + return true; + } catch (error) { + log.severe("Failed to delete local assets", error); + return false; } finally { _deleteInProgress = false; state = false; } - - return false; } - Future deleteRemoteOnlyAssets( + /// Delete remote asset only + /// + /// Default behavior is trashing the asset + Future deleteRemoteAssets( Iterable deleteAssets, { - bool force = false, + bool shouldDeletePermanently = false, }) async { _deleteInProgress = true; state = true; try { - final remoteDeleted = await _deleteRemoteAssets(deleteAssets, force); - if (remoteDeleted.isNotEmpty) { - final assetsToUpdate = force - - /// If force, only update merged only assets and remove remote assets - ? remoteDeleted - .where((e) => e.storage == AssetState.merged) - .map((e) { - e.remoteId = null; - return e; - }) - // If not force, trash everything - : remoteDeleted.where((e) => e.isRemote).map((e) { - e.isTrashed = true; - return e; - }); - - await _db.writeTxn(() async { - if (assetsToUpdate.isNotEmpty) { - await _db.assets.putAll(assetsToUpdate.toList()); - } - if (force) { - final remoteOnly = remoteDeleted - .where((e) => e.storage == AssetState.remote) - .map((e) => e.id) - .toList(); - await _db.exifInfos.deleteAll(remoteOnly); - await _db.assets.deleteAll(remoteOnly); - } - }); - return true; - } + await _assetService.deleteRemoteAssets( + deleteAssets, + shouldDeletePermanently: shouldDeletePermanently, + ); + return true; + } catch (error) { + log.severe("Failed to delete remote assets", error); + return false; } finally { _deleteInProgress = false; state = false; } - return false; } Future deleteAssets( @@ -183,111 +144,18 @@ class AssetNotifier extends StateNotifier { _deleteInProgress = true; state = true; try { - final hasLocal = deleteAssets.any((a) => a.storage != AssetState.remote); - final localDeleted = await _deleteLocalAssets(deleteAssets); - final remoteDeleted = (hasLocal && localDeleted.isNotEmpty) || !hasLocal - ? await _deleteRemoteAssets(deleteAssets, force) - : []; - if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) { - final dbIds = []; - final dbUpdates = []; - - // Local assets are removed - if (localDeleted.isNotEmpty) { - // Permanently remove local only assets from isar - dbIds.addAll( - deleteAssets - .where((a) => a.storage == AssetState.local) - .map((e) => e.id), - ); - - if (remoteDeleted.any((e) => e.isLocal)) { - // Force delete: Add all local assets including merged assets - if (force) { - dbIds.addAll(remoteDeleted.map((e) => e.id)); - // Soft delete: Remove local Id from asset and trash it - } else { - dbUpdates.addAll( - remoteDeleted.map((e) { - e.localId = null; - e.isTrashed = true; - return e; - }), - ); - } - } - } - - // Handle remote deletion - if (remoteDeleted.isNotEmpty) { - if (force) { - // Remove remote only assets - dbIds.addAll( - deleteAssets - .where((a) => a.storage == AssetState.remote) - .map((e) => e.id), - ); - // Local assets are not removed and there are merged assets - final hasLocal = remoteDeleted.any((e) => e.isLocal); - if (localDeleted.isEmpty && hasLocal) { - // Remove remote Id from local assets - dbUpdates.addAll( - remoteDeleted.map((e) { - e.remoteId = null; - // Remove from trashed if remote asset is removed - e.isTrashed = false; - return e; - }), - ); - } - } else { - dbUpdates.addAll( - remoteDeleted.map((e) { - e.isTrashed = true; - return e; - }), - ); - } - } - - await _db.writeTxn(() async { - await _db.assets.putAll(dbUpdates); - await _db.exifInfos.deleteAll(dbIds); - await _db.assets.deleteAll(dbIds); - }); - return true; - } + await _assetService.deleteAssets( + deleteAssets, + shouldDeletePermanently: force, + ); + return true; + } catch (error) { + log.severe("Failed to delete assets", error); + return false; } finally { _deleteInProgress = false; state = false; } - return false; - } - - Future> _deleteLocalAssets( - Iterable assetsToDelete, - ) async { - final List local = - assetsToDelete.where((a) => a.isLocal).map((a) => a.localId!).toList(); - // Delete asset from device - if (local.isNotEmpty) { - try { - return await _ref.read(assetMediaRepositoryProvider).deleteAll(local); - } catch (e, stack) { - log.severe("Failed to delete asset from device", e, stack); - } - } - return []; - } - - Future> _deleteRemoteAssets( - Iterable assetsToDelete, - bool? force, - ) async { - final Iterable remote = assetsToDelete.where((e) => e.isRemote); - - final isSuccess = await _assetService.deleteAssets(remote, force: force); - return isSuccess ? remote.toList() : []; } Future toggleFavorite(List assets, [bool? status]) { @@ -301,41 +169,40 @@ class AssetNotifier extends StateNotifier { } } -final assetProvider = StateNotifierProvider((ref) { - return AssetNotifier( - ref.watch(assetServiceProvider), - ref.watch(albumServiceProvider), - ref.watch(userServiceProvider), - ref.watch(syncServiceProvider), - ref.watch(dbProvider), - ref, - ); -}); - final assetDetailProvider = StreamProvider.autoDispose.family((ref, asset) async* { - yield await ref.watch(assetServiceProvider).loadExif(asset); - final db = ref.watch(dbProvider); - await for (final a in db.assets.watchObject(asset.id)) { - if (a != null) { - yield await ref.watch(assetServiceProvider).loadExif(a); + final assetService = ref.watch(assetServiceProvider); + yield await assetService.loadExif(asset); + + await for (final asset in assetService.watchAsset(asset.id)) { + if (asset != null) { + yield await ref.watch(assetServiceProvider).loadExif(asset); } } }); final assetWatcher = StreamProvider.autoDispose.family((ref, asset) { - final db = ref.watch(dbProvider); - return db.assets.watchObject(asset.id, fireImmediately: true); + final assetService = ref.watch(assetServiceProvider); + return assetService.watchAsset(asset.id, fireImmediately: true); }); final assetsProvider = StreamProvider.family( (ref, userId) { if (userId == null) return const Stream.empty(); ref.watch(localeProvider); - final query = _commonFilterAndSort( - _assets(ref).where().ownerIdEqualToAnyChecksum(userId), - ); + + final query = ref + .watch(dbProvider) + .assets + .where() + .ownerIdEqualToAnyChecksum(userId) + .filter() + .isArchivedEqualTo(false) + .isTrashedEqualTo(false) + .stackPrimaryAssetIdIsNull() + .sortByFileCreatedAtDesc(); + return renderListGenerator(query, ref); }, dependencies: [localeProvider], @@ -345,11 +212,17 @@ final multiUserAssetsProvider = StreamProvider.family>( (ref, userIds) { if (userIds.isEmpty) return const Stream.empty(); ref.watch(localeProvider); - final query = _commonFilterAndSort( - _assets(ref) - .where() - .anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)), - ); + final query = ref + .watch(dbProvider) + .assets + .where() + .anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)) + .filter() + .isArchivedEqualTo(false) + .isTrashedEqualTo(false) + .stackPrimaryAssetIdIsNull() + .sortByFileCreatedAtDesc(); + return renderListGenerator(query, ref); }, dependencies: [localeProvider], @@ -371,17 +244,3 @@ QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); } - -IsarCollection _assets(StreamProviderRef ref) => - ref.watch(dbProvider).assets; - -QueryBuilder _commonFilterAndSort( - QueryBuilder query, -) { - return query - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackPrimaryAssetIdIsNull() - .sortByFileCreatedAtDesc(); -} diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart index 8bbac853c7..8619970d4a 100644 --- a/mobile/lib/providers/trash.provider.dart +++ b/mobile/lib/providers/trash.provider.dart @@ -57,7 +57,7 @@ class TrashNotifier extends StateNotifier { final isRemoved = await _ref .read(assetProvider.notifier) - .deleteRemoteOnlyAssets(assetList, force: true); + .deleteRemoteAssets(assetList, shouldDeletePermanently: true); if (isRemoved) { final idsToRemove = diff --git a/mobile/lib/repositories/album.repository.dart b/mobile/lib/repositories/album.repository.dart index 041820f0f2..66104e95a5 100644 --- a/mobile/lib/repositories/album.repository.dart +++ b/mobile/lib/repositories/album.repository.dart @@ -155,6 +155,13 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository { return await query.findAll(); } + @override + Future clearTable() async { + await txn(() async { + await db.albums.clear(); + }); + } + @override Stream> watchRemoteAlbums() { return db.albums.where().remoteIdIsNotNull().watch(); diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index a207e15092..351f37c4df 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -57,7 +57,7 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { } @override - Future deleteById(List ids) => txn(() async { + Future deleteByIds(List ids) => txn(() async { await db.assets.deleteAll(ids); await db.exifInfos.deleteAll(ids); }); @@ -210,6 +210,18 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { .thenByFileCreatedAtDesc() .findAll(); } + + @override + Future clearTable() async { + await txn(() async { + await db.assets.clear(); + }); + } + + @override + Stream watchAsset(int id, {bool fireImmediately = false}) { + return db.assets.watchObject(id, fireImmediately: fireImmediately); + } } Future> _getMatchesImpl( diff --git a/mobile/lib/repositories/etag.repository.dart b/mobile/lib/repositories/etag.repository.dart index 9921b69f5e..93d98de28c 100644 --- a/mobile/lib/repositories/etag.repository.dart +++ b/mobile/lib/repositories/etag.repository.dart @@ -26,4 +26,11 @@ class ETagRepository extends DatabaseRepository implements IETagRepository { @override Future getById(String id) => db.eTags.getById(id); + + @override + Future clearTable() async { + await txn(() async { + await db.eTags.clear(); + }); + } } diff --git a/mobile/lib/repositories/exif_info.repository.dart b/mobile/lib/repositories/exif_info.repository.dart index 3ddb50104b..a70b216df1 100644 --- a/mobile/lib/repositories/exif_info.repository.dart +++ b/mobile/lib/repositories/exif_info.repository.dart @@ -28,4 +28,9 @@ class ExifInfoRepository extends DatabaseRepository await txn(() => db.exifInfos.putAll(exifInfos)); return exifInfos; } + + @override + Future clearTable() { + return txn(() => db.exifInfos.clear()); + } } diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart index bc9325c9e9..e490c7d8c1 100644 --- a/mobile/lib/repositories/user.repository.dart +++ b/mobile/lib/repositories/user.repository.dart @@ -57,4 +57,11 @@ class UserRepository extends DatabaseRepository implements IUserRepository { .or() .isarIdEqualTo(Store.get(StoreKey.currentUser).isarId) .findAll(); + + @override + Future clearTable() async { + await txn(() async { + await db.users.clear(); + }); + } } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 49931f0b85..778b47a7c9 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -310,7 +310,7 @@ class AlbumService { final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteById(idsToRemove); + await _assetRepository.deleteByIds(idsToRemove); } } else { await _albumRepository.delete(album.id); @@ -491,4 +491,8 @@ class AlbumService { } return null; } + + Future clearTable() async { + await _albumRepository.clearTable(); + } } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 6a9879f650..12552effcb 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -9,6 +9,7 @@ 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/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'; @@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.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'; @@ -43,6 +45,7 @@ final assetServiceProvider = Provider( ref.watch(userServiceProvider), ref.watch(backupServiceProvider), ref.watch(albumServiceProvider), + ref.watch(assetMediaRepositoryProvider), ), ); @@ -58,6 +61,7 @@ class AssetService { final UserService _userService; final BackupService _backupService; final AlbumService _albumService; + final IAssetMediaRepository _assetMediaRepository; final log = Logger('AssetService'); AssetService( @@ -72,6 +76,7 @@ class AssetService { this._userService, this._backupService, this._albumService, + this._assetMediaRepository, ); /// Checks the server for updated assets and updates the local database if @@ -158,30 +163,6 @@ class AssetService { } } - Future deleteAssets( - Iterable deleteAssets, { - bool? force = false, - }) async { - try { - final List payload = []; - - for (final asset in deleteAssets) { - payload.add(asset.remoteId!); - } - - await _apiService.assetsApi.deleteAssets( - AssetBulkDeleteDto( - ids: payload, - force: force, - ), - ); - return true; - } catch (error, stack) { - log.severe("Error while deleting assets", error, stack); - } - return false; - } - /// Loads the exif information from the database. If there is none, loads /// the exif info from the server (remote assets only) Future loadExif(Asset a) async { @@ -432,4 +413,105 @@ class AssetService { Future> getStackAssets(String stackId) { return _assetRepository.getStackAssets(stackId); } + + Future clearTable() { + return _assetRepository.clearTable(); + } + + /// Delete assets from local file system and unreference from the database + Future deleteLocalAssets(Iterable assets) async { + // Delete files from local gallery + final candidates = assets.where((asset) => asset.isLocal); + + final deletedIds = await _assetMediaRepository + .deleteAll(candidates.map((asset) => asset.localId!).toList()); + + // Modify local database by removing the reference to the local assets + if (deletedIds.isNotEmpty) { + // Delete records from local database + final isarIds = assets + .where((asset) => asset.storage == AssetState.local) + .map((asset) => asset.id) + .toList(); + await _assetRepository.deleteByIds(isarIds); + + // Modify Merged asset to be remote only + final updatedAssets = assets + .where((asset) => asset.storage == AssetState.merged) + .map((asset) { + asset.localId = null; + return asset; + }).toList(); + + await _assetRepository.updateAll(updatedAssets); + } + } + + /// Delete assets from the server and unreference from the database + Future deleteRemoteAssets( + Iterable assets, { + bool shouldDeletePermanently = false, + }) async { + final candidates = assets.where((a) => a.isRemote); + if (candidates.isEmpty) { + return; + } + + await _apiService.assetsApi.deleteAssets( + AssetBulkDeleteDto( + ids: candidates.map((a) => a.remoteId!).toList(), + force: shouldDeletePermanently, + ), + ); + + /// Update asset info bassed on the deletion type. + final payload = shouldDeletePermanently + ? assets + .where((asset) => asset.storage == AssetState.merged) + .map((asset) { + asset.remoteId = null; + return asset; + }) + : assets.where((asset) => asset.isRemote).map((asset) { + asset.isTrashed = true; + return asset; + }); + + await _assetRepository.transaction(() async { + await _assetRepository.updateAll(payload.toList()); + + if (shouldDeletePermanently) { + final remoteAssetIds = assets + .where((asset) => asset.storage == AssetState.remote) + .map((asset) => asset.id) + .toList(); + await _assetRepository.deleteByIds(remoteAssetIds); + } + }); + } + + /// Delete assets on both local file system and the server. + /// Unreference from the database. + Future deleteAssets( + Iterable assets, { + bool shouldDeletePermanently = false, + }) async { + final hasLocal = assets.any((asset) => asset.isLocal); + final hasRemote = assets.any((asset) => asset.isRemote); + + if (hasLocal) { + await deleteLocalAssets(assets); + } + + if (hasRemote) { + await deleteRemoteAssets( + assets, + shouldDeletePermanently: shouldDeletePermanently, + ); + } + } + + Stream watchAsset(int id, {bool fireImmediately = false}) { + return _assetRepository.watchAsset(id, fireImmediately: fireImmediately); + } } diff --git a/mobile/lib/services/etag.service.dart b/mobile/lib/services/etag.service.dart new file mode 100644 index 0000000000..6dd8a76bb3 --- /dev/null +++ b/mobile/lib/services/etag.service.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/etag.interface.dart'; +import 'package:immich_mobile/repositories/etag.repository.dart'; + +final etagServiceProvider = + Provider((ref) => ETagService(ref.watch(etagRepositoryProvider))); + +class ETagService { + final IETagRepository _eTagRepository; + + ETagService(this._eTagRepository); + + Future clearTable() { + return _eTagRepository.clearTable(); + } +} diff --git a/mobile/lib/services/exif.service.dart b/mobile/lib/services/exif.service.dart new file mode 100644 index 0000000000..2ce2b1ffa5 --- /dev/null +++ b/mobile/lib/services/exif.service.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/exif_info.interface.dart'; +import 'package:immich_mobile/repositories/exif_info.repository.dart'; + +final exifServiceProvider = + Provider((ref) => ExifService(ref.watch(exifInfoRepositoryProvider))); + +class ExifService { + final IExifInfoRepository _exifInfoRepository; + + ExifService(this._exifInfoRepository); + + Future clearTable() { + return _exifInfoRepository.clearTable(); + } +} diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 086ec097d1..ddca266006 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -286,7 +286,7 @@ class SyncService { } final idsToDelete = toRemove.map((e) => e.id).toList(); try { - await _assetRepository.deleteById(idsToDelete); + await _assetRepository.deleteByIds(idsToDelete); await upsertAssetsWithExif(toAdd + toUpdate); } catch (e) { _log.severe("Failed to sync remote assets to db", e); @@ -334,7 +334,7 @@ class SyncService { if (toDelete.isNotEmpty) { final List idsToRemove = sharedAssetsToRemove(toDelete, existing); if (idsToRemove.isNotEmpty) { - await _assetRepository.deleteById(idsToRemove); + await _assetRepository.deleteByIds(idsToRemove); } } else { assert(toDelete.isEmpty); @@ -531,7 +531,7 @@ class SyncService { ); if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { await _assetRepository.transaction(() async { - await _assetRepository.deleteById(toDelete); + await _assetRepository.deleteByIds(toDelete); await _assetRepository.updateAll(toUpdate); }); _log.info( @@ -826,7 +826,7 @@ class SyncService { final (toDelete, toUpdate) = _handleAssetRemoval(assets, [], remote: false); await _assetRepository.transaction(() async { - await _assetRepository.deleteById(toDelete); + await _assetRepository.deleteByIds(toDelete); await _assetRepository.updateAll(toUpdate); await _albumRepository.deleteAllLocal(); }); diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart index 13adcc4e7a..935a751e2a 100644 --- a/mobile/lib/services/user.service.dart +++ b/mobile/lib/services/user.service.dart @@ -103,4 +103,8 @@ class UserService { if (users == null) return false; return _syncService.syncUsersFromServer(users); } + + Future clearTable() { + return _userRepository.clearTable(); + } } diff --git a/mobile/lib/utils/db.dart b/mobile/lib/utils/db.dart deleted file mode 100644 index 4d405468fa..0000000000 --- a/mobile/lib/utils/db.dart +++ /dev/null @@ -1,18 +0,0 @@ -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:isar/isar.dart'; - -Future clearAssetsAndAlbums(Isar db) async { - await Store.delete(StoreKey.assetETag); - await db.writeTxn(() async { - await db.assets.clear(); - await db.exifInfos.clear(); - await db.albums.clear(); - await db.eTags.clear(); - await db.users.clear(); - }); -} diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 681f8a22ce..ecbda2f266 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,7 +1,11 @@ import 'dart:async'; +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/utils/db.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; import 'package:isar/isar.dart'; const int targetVersion = 8; @@ -14,6 +18,13 @@ Future migrateDatabaseIfNeeded(Isar db) async { } Future _migrateTo(Isar db, int version) async { - await clearAssetsAndAlbums(db); + await Store.delete(StoreKey.assetETag); + await db.writeTxn(() async { + await db.assets.clear(); + await db.exifInfos.clear(); + await db.albums.clear(); + await db.eTags.clear(); + await db.users.clear(); + }); await Store.put(StoreKey.version, version); } diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 6bcd6e5784..03d04b682f 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -200,24 +200,26 @@ class MultiselectGrid extends HookConsumerWidget { } } - void onDeleteLocal(bool onlyBackedUp) async { + void onDeleteLocal(bool isMergedAsset) async { processing.value = true; try { - // Select only the local assets from the selection - final localIds = selection.value.where((a) => a.isLocal).toList(); + final localAssets = selection.value.where((a) => a.isLocal).toList(); + + final toDelete = isMergedAsset + ? localAssets.where((e) => e.storage == AssetState.merged) + : localAssets; + + if (toDelete.isEmpty) { + return; + } - // Delete only the backed-up assets if 'onlyBackedUp' is true final isDeleted = await ref .read(assetProvider.notifier) - .deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp); + .deleteLocalAssets(toDelete.toList()); if (isDeleted) { - // Show a toast with the correct number of deleted assets - final deletedCount = localIds - .where( - (e) => !onlyBackedUp || e.isRemote, - ) // Only count backed-up assets - .length; + final deletedCount = + localAssets.where((e) => !isMergedAsset || e.isRemote).length; ImmichToast.show( context: context, @@ -226,7 +228,6 @@ class MultiselectGrid extends HookConsumerWidget { gravity: ToastGravity.BOTTOM, ); - // Reset the selection selectionEnabledHook.value = false; } } finally { @@ -234,7 +235,7 @@ class MultiselectGrid extends HookConsumerWidget { } } - void onDeleteRemote([bool force = false]) async { + void onDeleteRemote([bool shouldDeletePermanently = false]) async { processing.value = true; try { final toDelete = ownedRemoteSelection( @@ -242,13 +243,15 @@ class MultiselectGrid extends HookConsumerWidget { ownerErrorMessage: 'home_page_delete_err_partner'.tr(), ).toList(); - final isDeleted = await ref - .read(assetProvider.notifier) - .deleteRemoteOnlyAssets(toDelete, force: force); + final isDeleted = + await ref.read(assetProvider.notifier).deleteRemoteAssets( + toDelete, + shouldDeletePermanently: shouldDeletePermanently, + ); if (isDeleted) { ImmichToast.show( context: context, - msg: force + msg: shouldDeletePermanently ? 'assets_deleted_permanently_from_server' .tr(args: ["${toDelete.length}"]) : 'assets_trashed_from_server'.tr(args: ["${toDelete.length}"]), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index 218e17cbe1..94d413859e 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -134,7 +134,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ref.read(manualUploadProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup(); - ref.read(assetProvider.notifier).clearAllAsset(); + ref.read(assetProvider.notifier).clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); context.replaceRoute(const LoginRoute()); }, diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index fbb8fd927b..7c375844b8 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -85,7 +85,7 @@ class ChangePasswordForm extends HookConsumerWidget { ref.read(backupProvider.notifier).cancelBackup(); await ref .read(assetProvider.notifier) - .clearAllAsset(); + .clearAllAssets(); ref.read(websocketProvider.notifier).disconnect(); AutoRouter.of(context).back(); diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index c85487c7d0..5eca5016fd 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -105,7 +105,7 @@ void main() { when(() => assetRepository.getAllByOwnerIdChecksum(any(), any())) .thenAnswer((_) async => [initialAssets[3], null, null]); when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []); - when(() => assetRepository.deleteById(any())).thenAnswer((_) async {}); + when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {}); when(() => exifInfoRepository.updateAll(any())) .thenAnswer((_) async => []); when(() => assetRepository.transaction(any())).thenAnswer(