You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	refactor(mobile): delete local button in new timeline (#19961)
* delete local action button * include source * move prompt * batch
This commit is contained in:
		| @@ -749,6 +749,7 @@ | ||||
|   "delete_key": "Delete key", | ||||
|   "delete_library": "Delete Library", | ||||
|   "delete_link": "Delete link", | ||||
|   "delete_local_action_prompt": "{count} deleted locally", | ||||
|   "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", | ||||
|   "delete_local_dialog_ok_force": "Delete Anyway", | ||||
|   "delete_others": "Delete others", | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; | ||||
| @@ -43,4 +44,16 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> delete(List<String> ids) { | ||||
|     if (ids.isEmpty) { | ||||
|       return Future.value(); | ||||
|     } | ||||
|  | ||||
|     return _db.batch((batch) { | ||||
|       for (final slice in ids.slices(32000)) { | ||||
|         batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice)); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,42 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/enums.dart'; | ||||
| import 'package:immich_mobile/extensions/translate_extensions.dart'; | ||||
| import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; | ||||
| import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; | ||||
| import 'package:immich_mobile/widgets/common/immich_toast.dart'; | ||||
|  | ||||
| class DeleteLocalActionButton extends ConsumerWidget { | ||||
|   const DeleteLocalActionButton({super.key}); | ||||
|   final ActionSource source; | ||||
|  | ||||
|   const DeleteLocalActionButton({super.key, required this.source}); | ||||
|  | ||||
|   void _onTap(BuildContext context, WidgetRef ref) async { | ||||
|     if (!context.mounted) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final result = await ref.read(actionProvider.notifier).deleteLocal(source); | ||||
|     ref.read(multiSelectProvider.notifier).reset(); | ||||
|  | ||||
|     final successMessage = 'delete_local_action_prompt'.t( | ||||
|       context: context, | ||||
|       args: {'count': result.count.toString()}, | ||||
|     ); | ||||
|  | ||||
|     if (context.mounted) { | ||||
|       ImmichToast.show( | ||||
|         context: context, | ||||
|         msg: result.success | ||||
|             ? successMessage | ||||
|             : 'scaffold_body_error_occurred'.t(context: context), | ||||
|         gravity: ToastGravity.BOTTOM, | ||||
|         toastType: result.success ? ToastType.success : ToastType.error, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -12,6 +44,7 @@ class DeleteLocalActionButton extends ConsumerWidget { | ||||
|       maxWidth: 95.0, | ||||
|       iconData: Icons.no_cell_outlined, | ||||
|       label: "control_bottom_app_bar_delete_from_local".t(context: context), | ||||
|       onPressed: () => _onTap(context, ref), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -58,7 +58,7 @@ class AssetDetailBottomSheet extends ConsumerWidget { | ||||
|         ), | ||||
|       ], | ||||
|       if (asset.storage == AssetState.local) ...[ | ||||
|         const DeleteLocalActionButton(), | ||||
|         const DeleteLocalActionButton(source: ActionSource.viewer), | ||||
|         const UploadActionButton(), | ||||
|       ], | ||||
|     ]; | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class ArchiveBottomSheet extends ConsumerWidget { | ||||
|           const StackActionButton(), | ||||
|         ], | ||||
|         if (multiselect.hasLocal) ...[ | ||||
|           const DeleteLocalActionButton(), | ||||
|           const DeleteLocalActionButton(source: ActionSource.timeline), | ||||
|           const UploadActionButton(), | ||||
|         ], | ||||
|       ], | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class FavoriteBottomSheet extends ConsumerWidget { | ||||
|           const StackActionButton(), | ||||
|         ], | ||||
|         if (multiselect.hasLocal) ...[ | ||||
|           const DeleteLocalActionButton(), | ||||
|           const DeleteLocalActionButton(source: ActionSource.timeline), | ||||
|           const UploadActionButton(), | ||||
|         ], | ||||
|       ], | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class GeneralBottomSheet extends ConsumerWidget { | ||||
|           const StackActionButton(), | ||||
|         ], | ||||
|         if (multiselect.hasLocal) ...[ | ||||
|           const DeleteLocalActionButton(), | ||||
|           const DeleteLocalActionButton(source: ActionSource.timeline), | ||||
|           const UploadActionButton(), | ||||
|         ], | ||||
|       ], | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/enums.dart'; | ||||
| import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; | ||||
| import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; | ||||
| import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; | ||||
| @@ -16,7 +17,7 @@ class LocalAlbumBottomSheet extends ConsumerWidget { | ||||
|       shouldCloseOnMinExtent: false, | ||||
|       actions: [ | ||||
|         ShareActionButton(), | ||||
|         DeleteLocalActionButton(), | ||||
|         DeleteLocalActionButton(source: ActionSource.timeline), | ||||
|         UploadActionButton(), | ||||
|       ], | ||||
|     ); | ||||
|   | ||||
| @@ -55,7 +55,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget { | ||||
|           const StackActionButton(), | ||||
|         ], | ||||
|         if (multiselect.hasLocal) ...[ | ||||
|           const DeleteLocalActionButton(), | ||||
|           const DeleteLocalActionButton(source: ActionSource.timeline), | ||||
|           const UploadActionButton(), | ||||
|         ], | ||||
|         RemoveFromAlbumActionButton( | ||||
|   | ||||
| @@ -41,7 +41,13 @@ class ActionNotifier extends Notifier<void> { | ||||
|   } | ||||
|  | ||||
|   List<String> _getRemoteIdsForSource(ActionSource source) { | ||||
|     return _getIdsForSource<RemoteAsset>(source).toIds().toList(); | ||||
|     return _getIdsForSource<RemoteAsset>(source) | ||||
|         .toIds() | ||||
|         .toList(growable: false); | ||||
|   } | ||||
|  | ||||
|   List<String> _getLocalIdsForSource(ActionSource source) { | ||||
|     return _getIdsForSource<LocalAsset>(source).toIds().toList(growable: false); | ||||
|   } | ||||
|  | ||||
|   List<String> _getOwnedRemoteForSource(ActionSource source) { | ||||
| @@ -49,23 +55,22 @@ class ActionNotifier extends Notifier<void> { | ||||
|     return _getIdsForSource<RemoteAsset>(source) | ||||
|         .ownedAssets(ownerId) | ||||
|         .toIds() | ||||
|         .toList(); | ||||
|         .toList(growable: false); | ||||
|   } | ||||
|  | ||||
|   Iterable<T> _getIdsForSource<T extends BaseAsset>(ActionSource source) { | ||||
|     final Set<BaseAsset> assets = switch (source) { | ||||
|       ActionSource.timeline => | ||||
|         ref.read(multiSelectProvider.select((s) => s.selectedAssets)), | ||||
|       ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, | ||||
|       ActionSource.viewer => switch (ref.read(currentAssetNotifier)) { | ||||
|           BaseAsset asset => {asset}, | ||||
|           null => {}, | ||||
|           null => const {}, | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     return switch (T) { | ||||
|       const (RemoteAsset) => assets.whereType<RemoteAsset>(), | ||||
|       const (LocalAsset) => assets.whereType<LocalAsset>(), | ||||
|       _ => <T>[], | ||||
|       _ => const [], | ||||
|     } as Iterable<T>; | ||||
|   } | ||||
|  | ||||
| @@ -207,6 +212,21 @@ class ActionNotifier extends Notifier<void> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<ActionResult> deleteLocal(ActionSource source) async { | ||||
|     final ids = _getLocalIdsForSource(source); | ||||
|     try { | ||||
|       await _service.deleteLocal(ids); | ||||
|       return ActionResult(count: ids.length, success: true); | ||||
|     } catch (error, stack) { | ||||
|       _logger.severe('Failed to delete assets', error, stack); | ||||
|       return ActionResult( | ||||
|         count: ids.length, | ||||
|         success: false, | ||||
|         error: error.toString(), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<ActionResult?> editLocation( | ||||
|     ActionSource source, | ||||
|     BuildContext context, | ||||
| @@ -252,7 +272,11 @@ extension on Iterable<RemoteAsset> { | ||||
|   Iterable<String> toIds() => map((e) => e.id); | ||||
|  | ||||
|   Iterable<RemoteAsset> ownedAssets(String? ownerId) { | ||||
|     if (ownerId == null) return []; | ||||
|     if (ownerId == null) return const []; | ||||
|     return whereType<RemoteAsset>().where((a) => a.ownerId == ownerId); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension on Iterable<LocalAsset> { | ||||
|   Iterable<String> toIds() => map((e) => e.id); | ||||
| } | ||||
|   | ||||
| @@ -2,11 +2,13 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/constants/enums.dart'; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; | ||||
| import 'package:immich_mobile/repositories/asset_api.repository.dart'; | ||||
| import 'package:immich_mobile/repositories/asset_media.repository.dart'; | ||||
| import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/widgets/common/location_picker.dart'; | ||||
| @@ -17,22 +19,28 @@ final actionServiceProvider = Provider<ActionService>( | ||||
|   (ref) => ActionService( | ||||
|     ref.watch(assetApiRepositoryProvider), | ||||
|     ref.watch(remoteAssetRepositoryProvider), | ||||
|     ref.watch(localAssetRepository), | ||||
|     ref.watch(driftAlbumApiRepositoryProvider), | ||||
|     ref.watch(remoteAlbumRepository), | ||||
|     ref.watch(assetMediaRepositoryProvider), | ||||
|   ), | ||||
| ); | ||||
|  | ||||
| class ActionService { | ||||
|   final AssetApiRepository _assetApiRepository; | ||||
|   final RemoteAssetRepository _remoteAssetRepository; | ||||
|   final DriftLocalAssetRepository _localAssetRepository; | ||||
|   final DriftAlbumApiRepository _albumApiRepository; | ||||
|   final DriftRemoteAlbumRepository _remoteAlbumRepository; | ||||
|   final AssetMediaRepository _assetMediaRepository; | ||||
|  | ||||
|   const ActionService( | ||||
|     this._assetApiRepository, | ||||
|     this._remoteAssetRepository, | ||||
|     this._localAssetRepository, | ||||
|     this._albumApiRepository, | ||||
|     this._remoteAlbumRepository, | ||||
|     this._assetMediaRepository, | ||||
|   ); | ||||
|  | ||||
|   Future<void> shareLink(List<String> remoteIds, BuildContext context) async { | ||||
| @@ -107,6 +115,11 @@ class ActionService { | ||||
|     await _remoteAssetRepository.delete(remoteIds); | ||||
|   } | ||||
|  | ||||
|   Future<void> deleteLocal(List<String> localIds) async { | ||||
|     await _assetMediaRepository.deleteAll(localIds); | ||||
|     await _localAssetRepository.delete(localIds); | ||||
|   } | ||||
|  | ||||
|   Future<bool> editLocation( | ||||
|     List<String> remoteIds, | ||||
|     BuildContext context, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user