From dbee1337645b1ea1cc854784283499fd707b92ba Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:50:14 +0530 Subject: [PATCH] fix: show dialog before delete local action (#22280) * fix: show dialog on delete local action # Conflicts: # mobile/lib/repositories/asset_media.repository.dart * button style --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- .../domain/models/asset/base_asset.model.dart | 2 + .../models/asset/local_asset.model.dart | 12 +++- .../models/asset/remote_asset.model.dart | 12 +++- .../delete_local_action_button.widget.dart | 13 ++++- .../infrastructure/action.provider.dart | 11 +++- .../repositories/asset_media.repository.dart | 1 + .../lib/widgets/asset_grid/delete_dialog.dart | 58 ++++++++++++------- 7 files changed, 80 insertions(+), 29 deletions(-) diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 4d40be2d32..5774a13c90 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -56,6 +56,8 @@ sealed class BaseAsset { // Overridden in subclasses AssetState get storage; + String? get localId; + String? get remoteId; String get heroTag; @override diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index 9cd20acb0a..6f2f4c06ba 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -2,12 +2,12 @@ part of 'base_asset.model.dart'; class LocalAsset extends BaseAsset { final String id; - final String? remoteId; + final String? remoteAssetId; final int orientation; const LocalAsset({ required this.id, - this.remoteId, + String? remoteId, required super.name, super.checksum, required super.type, @@ -19,7 +19,13 @@ class LocalAsset extends BaseAsset { super.isFavorite = false, super.livePhotoVideoId, this.orientation = 0, - }); + }) : remoteAssetId = remoteId; + + @override + String? get localId => id; + + @override + String? get remoteId => remoteAssetId; @override AssetState get storage => remoteId == null ? AssetState.local : AssetState.merged; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 8648255167..4974dc9118 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -5,7 +5,7 @@ enum AssetVisibility { timeline, hidden, archive, locked } // Model for an asset stored in the server class RemoteAsset extends BaseAsset { final String id; - final String? localId; + final String? localAssetId; final String? thumbHash; final AssetVisibility visibility; final String ownerId; @@ -13,7 +13,7 @@ class RemoteAsset extends BaseAsset { const RemoteAsset({ required this.id, - this.localId, + String? localId, required super.name, required this.ownerId, required super.checksum, @@ -28,7 +28,13 @@ class RemoteAsset extends BaseAsset { this.visibility = AssetVisibility.timeline, super.livePhotoVideoId, this.stackId, - }); + }) : localAssetId = localId; + + @override + String? get localId => localAssetId; + + @override + String? get remoteId => id; @override AssetState get storage => localId == null ? AssetState.remote : AssetState.merged; diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart index cccdee9b3a..9445daa2ad 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// This delete action has the following behavior: @@ -22,7 +23,17 @@ class DeleteLocalActionButton extends ConsumerWidget { return; } - final result = await ref.read(actionProvider.notifier).deleteLocal(source); + bool? backedUpOnly = await showDialog( + context: context, + builder: (BuildContext context) => DeleteLocalOnlyDialog(onDeleteLocal: (_) {}), + ); + + if (backedUpOnly == null) { + // User cancelled the dialog + return; + } + + final result = await ref.read(actionProvider.notifier).deleteLocal(source, backedUpOnly); ref.read(multiSelectProvider.notifier).reset(); if (source == ActionSource.viewer) { diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 21d76201c1..9f38f4b0ec 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -260,8 +260,15 @@ class ActionNotifier extends Notifier { } } - Future deleteLocal(ActionSource source) async { - final ids = _getLocalIdsForSource(source); + Future deleteLocal(ActionSource source, bool backedUpOnly) async { + final List ids; + if (backedUpOnly) { + final assets = _getAssets(source); + ids = assets.where((asset) => asset.storage == AssetState.merged).map((asset) => asset.localId!).toList(); + } else { + ids = _getLocalIdsForSource(source); + } + try { final deletedCount = await _service.deleteLocal(ids); return ActionResult(count: deletedCount, success: true); diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 8336d2341d..f71c919373 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -22,6 +22,7 @@ final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref. class AssetMediaRepository { final AssetApiRepository _assetApiRepository; + static final Logger _log = Logger("AssetMediaRepository"); const AssetMediaRepository(this._assetApiRepository); diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index e7c7775e54..adb22889a8 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -22,12 +22,12 @@ class DeleteLocalOnlyDialog extends StatelessWidget { @override Widget build(BuildContext context) { void onDeleteBackedUpOnly() { - context.pop(); + context.pop(true); onDeleteLocal(true); } void onForceDelete() { - context.pop(); + context.pop(false); onDeleteLocal(false); } @@ -36,26 +36,44 @@ class DeleteLocalOnlyDialog extends StatelessWidget { title: const Text("delete_dialog_title").tr(), content: const Text("delete_dialog_alert_local_non_backed_up").tr(), actions: [ - TextButton( - onPressed: () => context.pop(), - child: Text( - "cancel", - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: () => context.pop(), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.surfaceDim, + foregroundColor: context.primaryColor, + ), + child: const Text("cancel", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + ), ), - TextButton( - onPressed: onDeleteBackedUpOnly, - child: Text( - "delete_local_dialog_ok_backed_up_only", - style: TextStyle(color: context.colorScheme.tertiary, fontWeight: FontWeight.bold), - ).tr(), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + + child: FilledButton( + onPressed: onDeleteBackedUpOnly, + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.errorContainer, + foregroundColor: context.colorScheme.onErrorContainer, + ), + child: const Text( + "delete_local_dialog_ok_backed_up_only", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + ), ), - TextButton( - onPressed: onForceDelete, - child: Text( - "delete_local_dialog_ok_force", - style: TextStyle(color: Colors.red[400], fontWeight: FontWeight.bold), - ).tr(), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: onForceDelete, + style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white), + child: const Text("delete_local_dialog_ok_force", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + ), ), ], );