From 17a2043e765b9e1e2c0efa72785895200a20e3f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Feb 2025 22:14:41 -0600 Subject: [PATCH] refactor(mobile): trash provider (#16219) * refactor(mobile): trash provider * refactor(mobile): trash provider * pr feedback --- mobile/analysis_options.yaml | 2 +- mobile/lib/interfaces/asset.interface.dart | 5 + mobile/lib/pages/library/trash.page.dart | 5 +- mobile/lib/providers/trash.provider.dart | 136 ++---------------- mobile/lib/repositories/asset.repository.dart | 26 ++++ mobile/lib/services/trash.service.dart | 99 ++++++++----- .../widgets/asset_viewer/gallery_app_bar.dart | 3 +- mobile/openapi/devtools_options.yaml | 3 + 8 files changed, 115 insertions(+), 164 deletions(-) create mode 100644 mobile/openapi/devtools_options.yaml diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index ffeccbdd50..8ae9edef0d 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -79,7 +79,7 @@ custom_lint: - 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/{archive,asset,authentication,db,favorite,partner,user}.provider.dart - lib/providers/{asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart - import_rule_openapi: diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart index 4096a55061..b56c99a711 100644 --- a/mobile/lib/interfaces/asset.interface.dart +++ b/mobile/lib/interfaces/asset.interface.dart @@ -2,6 +2,7 @@ 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/interfaces/database.interface.dart'; +import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; abstract interface class IAssetRepository implements IDatabaseRepository { Future getByRemoteId(String id); @@ -63,6 +64,10 @@ abstract interface class IAssetRepository implements IDatabaseRepository { Future clearTable(); Stream watchAsset(int id, {bool fireImmediately = false}); + + Future> getTrashAssets(int userId); + + Stream getTrashRenderListStream(int userId); } enum AssetSort { checksum, ownerIdChecksum } diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart index 61c87e19a1..7322b00579 100644 --- a/mobile/lib/pages/library/trash.page.dart +++ b/mobile/lib/pages/library/trash.page.dart @@ -6,6 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/providers/trash.provider.dart'; @@ -67,8 +68,8 @@ class TrashPage extends HookConsumerWidget { try { if (selection.value.isNotEmpty) { final isRemoved = await ref - .read(trashProvider.notifier) - .removeAssets(selection.value); + .read(assetProvider.notifier) + .deleteAssets(selection.value, force: true); if (isRemoved) { if (context.mounted) { diff --git a/mobile/lib/providers/trash.provider.dart b/mobile/lib/providers/trash.provider.dart index 8619970d4a..0f7ae780a0 100644 --- a/mobile/lib/providers/trash.provider.dart +++ b/mobile/lib/providers/trash.provider.dart @@ -2,150 +2,44 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/trash.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/sync.service.dart'; -import 'package:immich_mobile/utils/renderlist_generator.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; class TrashNotifier extends StateNotifier { - final Isar _db; - final Ref _ref; final TrashService _trashService; final _log = Logger('TrashNotifier'); TrashNotifier( this._trashService, - this._db, - this._ref, ) : super(false); Future emptyTrash() async { try { - final user = _ref.read(currentUserProvider); - if (user == null) { - return; - } await _trashService.emptyTrash(); - - final idsToRemove = await _db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(user.isarId) - .isTrashedEqualTo(true) - .remoteIdProperty() - .findAll(); - - // TODO: handle local asset removal on emptyTrash - _ref - .read(syncServiceProvider) - .handleRemoteAssetRemoval(idsToRemove.cast().toList()); + state = true; } catch (error, stack) { _log.severe("Cannot empty trash", error, stack); + state = false; } } - Future removeAssets(Iterable assetList) async { - try { - final user = _ref.read(currentUserProvider); - if (user == null) { - return false; - } - - final isRemoved = await _ref - .read(assetProvider.notifier) - .deleteRemoteAssets(assetList, shouldDeletePermanently: true); - - if (isRemoved) { - final idsToRemove = - assetList.where((a) => a.isRemote).map((a) => a.remoteId!).toList(); - - _ref - .read(syncServiceProvider) - .handleRemoteAssetRemoval(idsToRemove.cast().toList()); - } - - return isRemoved; - } catch (error, stack) { - _log.severe("Cannot remove assets", error, stack); - } - return false; - } - - Future restoreAsset(Asset asset) async { - try { - final result = await _trashService.restoreAsset(asset); - - if (result) { - final remoteAsset = asset.isRemote; - - asset.isTrashed = false; - - if (remoteAsset) { - await _db.writeTxn(() async { - await _db.assets.put(asset); - }); - } - return true; - } - } catch (error, stack) { - _log.severe("Cannot restore asset", error, stack); - } - return false; - } - Future restoreAssets(Iterable assetList) async { try { - final result = await _trashService.restoreAssets(assetList); - - if (result) { - final remoteAssets = assetList.where((a) => a.isRemote).toList(); - - final updatedAssets = remoteAssets.map((e) { - e.isTrashed = false; - return e; - }).toList(); - - await _db.writeTxn(() async { - await _db.assets.putAll(updatedAssets); - }); - return true; - } + await _trashService.restoreAssets(assetList); + return true; } catch (error, stack) { _log.severe("Cannot restore assets", error, stack); + return false; } - return false; } Future restoreTrash() async { try { - final user = _ref.read(currentUserProvider); - if (user == null) { - return; - } await _trashService.restoreTrash(); - - final assets = await _db.assets - .where() - .remoteIdIsNotNull() - .filter() - .ownerIdEqualTo(user.isarId) - .isTrashedEqualTo(true) - .findAll(); - - final updatedAssets = assets.map((e) { - e.isTrashed = false; - return e; - }).toList(); - - await _db.writeTxn(() async { - await _db.assets.putAll(updatedAssets); - }); + state = true; } catch (error, stack) { _log.severe("Cannot restore trash", error, stack); + state = false; } } } @@ -153,20 +47,14 @@ class TrashNotifier extends StateNotifier { final trashProvider = StateNotifierProvider((ref) { return TrashNotifier( ref.watch(trashServiceProvider), - ref.watch(dbProvider), - ref, ); }); final trashedAssetsProvider = StreamProvider((ref) { final user = ref.read(currentUserProvider); - if (user == null) return const Stream.empty(); - final query = ref - .watch(dbProvider) - .assets - .filter() - .ownerIdEqualTo(user.isarId) - .isTrashedEqualTo(true) - .sortByFileCreatedAtDesc(); - return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none); + if (user == null) { + return const Stream.empty(); + } + + return ref.watch(trashServiceProvider).getRenderListGenerator(user.isarId); }); diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart index 351f37c4df..55add8cf96 100644 --- a/mobile/lib/repositories/asset.repository.dart +++ b/mobile/lib/repositories/asset.repository.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/entities/ios_device_asset.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:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:isar/isar.dart'; final assetRepositoryProvider = @@ -222,6 +223,31 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository { Stream watchAsset(int id, {bool fireImmediately = false}) { return db.assets.watchObject(id, fireImmediately: fireImmediately); } + + @override + Future> getTrashAssets(int userId) { + return db.assets + .where() + .remoteIdIsNotNull() + .filter() + .ownerIdEqualTo(userId) + .isTrashedEqualTo(true) + .findAll(); + } + + @override + Stream getTrashRenderListStream(int userId) async* { + final query = db.assets + .filter() + .ownerIdEqualTo(userId) + .isTrashedEqualTo(true) + .sortByFileCreatedAtDesc(); + + yield await RenderList.fromQuery(query, GroupAssetsBy.none); + await for (final _ in query.watchLazy()) { + yield await RenderList.fromQuery(query, GroupAssetsBy.none); + } + } } Future> _getMatchesImpl( diff --git a/mobile/lib/services/trash.service.dart b/mobile/lib/services/trash.service.dart index 9342b1f1e4..690031d934 100644 --- a/mobile/lib/services/trash.service.dart +++ b/mobile/lib/services/trash.service.dart @@ -1,62 +1,89 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; +import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:openapi/api.dart'; final trashServiceProvider = Provider((ref) { return TrashService( ref.watch(apiServiceProvider), + ref.watch(assetRepositoryProvider), + ref.watch(userRepositoryProvider), ); }); class TrashService { - final _log = Logger("TrashService"); - final ApiService _apiService; + final IAssetRepository _assetRepository; + final IUserRepository _userRepository; - TrashService(this._apiService); + TrashService(this._apiService, this._assetRepository, this._userRepository); - Future restoreAssets(Iterable assetList) async { - try { - List remoteIds = - assetList.where((a) => a.isRemote).map((e) => e.remoteId!).toList(); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteIds)); - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } - } + Future restoreAssets(Iterable assetList) async { + final remoteAssets = assetList.where((a) => a.isRemote); + await _apiService.trashApi.restoreAssets( + BulkIdsDto(ids: remoteAssets.map((e) => e.remoteId!).toList()), + ); - Future restoreAsset(Asset asset) async { - try { - if (asset.isRemote) { - List remoteId = [asset.remoteId!]; + final updatedAssets = remoteAssets.map((asset) { + asset.isTrashed = false; + return asset; + }).toList(); - await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteId)); - } - return true; - } catch (error, stack) { - _log.severe("Cannot restore assets", error, stack); - return false; - } + await _assetRepository.updateAll(updatedAssets); } Future emptyTrash() async { - try { - await _apiService.trashApi.emptyTrash(); - } catch (error, stack) { - _log.severe("Cannot empty trash", error, stack); - } + final user = await _userRepository.me(); + + await _apiService.trashApi.emptyTrash(); + + final trashedAssets = await _assetRepository.getTrashAssets(user.isarId); + final ids = trashedAssets.map((e) => e.remoteId!).toList(); + + await _assetRepository.transaction(() async { + await _assetRepository.deleteAllByRemoteId( + ids, + state: AssetState.remote, + ); + + final merged = await _assetRepository.getAllByRemoteId( + ids, + state: AssetState.merged, + ); + if (merged.isEmpty) { + return; + } + + for (final Asset asset in merged) { + asset.remoteId = null; + asset.isTrashed = false; + } + + await _assetRepository.updateAll(merged); + }); } Future restoreTrash() async { - try { - await _apiService.trashApi.restoreTrash(); - } catch (error, stack) { - _log.severe("Cannot restore trash", error, stack); - } + final user = await _userRepository.me(); + + await _apiService.trashApi.restoreTrash(); + + final trashedAssets = await _assetRepository.getTrashAssets(user.isarId); + final updatedAssets = trashedAssets.map((asset) { + asset.isTrashed = false; + return asset; + }).toList(); + + await _assetRepository.updateAll(updatedAssets); + } + + Stream getRenderListGenerator(int userId) { + return _assetRepository.getTrashRenderListStream(userId); } } diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index f7e2158ea9..2cb90da599 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -49,7 +49,8 @@ class GalleryAppBar extends ConsumerWidget { } handleRestore(Asset asset) async { - final result = await ref.read(trashProvider.notifier).restoreAsset(asset); + final result = + await ref.read(trashProvider.notifier).restoreAssets([asset]); if (result && context.mounted) { ImmichToast.show( diff --git a/mobile/openapi/devtools_options.yaml b/mobile/openapi/devtools_options.yaml new file mode 100644 index 0000000000..fa0b357c4f --- /dev/null +++ b/mobile/openapi/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: