chore: delete action button (#20261)

This commit is contained in:
Alex 2025-07-26 13:51:18 -05:00 committed by GitHub
parent b14c768208
commit 3a5d82f790
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 162 additions and 9 deletions

View File

@ -744,7 +744,8 @@
"default_locale": "Default Locale", "default_locale": "Default Locale",
"default_locale_description": "Format dates and numbers based on your browser locale", "default_locale_description": "Format dates and numbers based on your browser locale",
"delete": "Delete", "delete": "Delete",
"delete_action_prompt": "{count} deleted permanently", "delete_action_confirmation_message": "Are you sure you want to delete this asset? This action will move the asset to the server's trash and will prompt if you want to delete it locally",
"delete_action_prompt": "{count} deleted",
"delete_album": "Delete album", "delete_album": "Delete album",
"delete_api_key_prompt": "Are you sure you want to delete this API key?", "delete_api_key_prompt": "Are you sure you want to delete this API key?",
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
@ -762,6 +763,8 @@
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway", "delete_local_dialog_ok_force": "Delete Anyway",
"delete_others": "Delete others", "delete_others": "Delete others",
"delete_permanently": "Delete permanently",
"delete_permanently_action_prompt": "{count} deleted permanently",
"delete_shared_link": "Delete shared link", "delete_shared_link": "Delete shared link",
"delete_shared_link_dialog_title": "Delete Shared Link", "delete_shared_link_dialog_title": "Delete Shared Link",
"delete_tag": "Delete tag", "delete_tag": "Delete tag",

View File

@ -0,0 +1,85 @@
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/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/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/common/immich_toast.dart';
/// This delete action has the following behavior:
/// - Set the deletedAt information, put the asset in the trash in the server
/// which will be permanently deleted after the number of days configure by the admin
/// - Prompt to delete the asset locally
class DeleteActionButton extends ConsumerWidget {
final ActionSource source;
final bool showConfirmation;
const DeleteActionButton({super.key, required this.source, this.showConfirmation = false});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
if (showConfirmation) {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('delete'.t(context: context)),
content: Text('delete_action_confirmation_message'.t(context: context)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text('cancel'.t(context: context)),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
'confirm'.t(context: context),
style: TextStyle(
color: context.colorScheme.error,
),
),
),
],
),
);
if (confirm != true) return;
}
final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source);
ref.read(multiSelectProvider.notifier).reset();
if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent());
}
final successMessage = 'delete_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) {
return BaseActionButton(
maxWidth: 110.0,
iconData: Icons.delete_sweep_outlined,
label: "delete".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@ -10,6 +10,8 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
/// - Prompt to delete the asset locally
class DeleteLocalActionButton extends ConsumerWidget { class DeleteLocalActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
@ -27,6 +29,10 @@ class DeleteLocalActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent()); EventStream.shared.emit(const ViewerReloadAssetEvent());
} }
if (result.count == 0) {
return;
}
final successMessage = 'delete_local_action_prompt'.t( final successMessage = 'delete_local_action_prompt'.t(
context: context, context: context,
args: {'count': result.count.toString()}, args: {'count': result.count.toString()},

View File

@ -10,6 +10,9 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
/// - Delete permanently on the server
/// - Prompt to delete the asset locally
class DeletePermanentActionButton extends ConsumerWidget { class DeletePermanentActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
@ -27,7 +30,7 @@ class DeletePermanentActionButton extends ConsumerWidget {
EventStream.shared.emit(const ViewerReloadAssetEvent()); EventStream.shared.emit(const ViewerReloadAssetEvent());
} }
final successMessage = 'delete_action_prompt'.t( final successMessage = 'delete_permanently_action_prompt'.t(
context: context, context: context,
args: {'count': result.count.toString()}, args: {'count': result.count.toString()},
); );
@ -47,7 +50,7 @@ class DeletePermanentActionButton extends ConsumerWidget {
return BaseActionButton( return BaseActionButton(
maxWidth: 110.0, maxWidth: 110.0,
iconData: Icons.delete_forever, iconData: Icons.delete_forever,
label: "delete_dialog_title".t(context: context), label: "delete_permanently".t(context: context),
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
); );
} }

View File

@ -7,6 +7,11 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
/// - Delete permanently on the server
/// - Prompt to delete the asset locally
///
/// This action is used when the asset is selected in multi-selection mode in the trash page
class DeleteTrashActionButton extends ConsumerWidget { class DeleteTrashActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;

View File

@ -10,6 +10,9 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
/// - Set the deletedAt information, put the asset in the trash in the server
/// which will be permanently deleted after the number of days configure by the admin
class TrashActionButton extends ConsumerWidget { class TrashActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;

View File

@ -4,6 +4,8 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.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/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
@ -39,6 +41,14 @@ class ViewerBottomBar extends ConsumerWidget {
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(source: ActionSource.viewer),
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer), if (asset.hasRemote && isOwner) const ArchiveActionButton(source: ActionSource.viewer),
asset.isLocalOnly
? const DeleteLocalActionButton(
source: ActionSource.viewer,
)
: const DeleteActionButton(
source: ActionSource.viewer,
showConfirmation: true,
),
]; ];
return IgnorePointer( return IgnorePointer(
@ -60,7 +70,7 @@ class ViewerBottomBar extends ConsumerWidget {
), ),
), ),
child: Container( child: Container(
height: context.padding.bottom + (asset.isVideo ? 160 : 80), height: context.padding.bottom + (asset.isVideo ? 160 : 90),
color: Colors.black.withAlpha(125), color: Colors.black.withAlpha(125),
padding: EdgeInsets.only(bottom: context.padding.bottom), padding: EdgeInsets.only(bottom: context.padding.bottom),
child: Column( child: Column(

View File

@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
@ -56,6 +57,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
isTrashEnable isTrashEnable
? const TrashActionButton(source: ActionSource.viewer) ? const TrashActionButton(source: ActionSource.viewer)
: const DeletePermanentActionButton(source: ActionSource.viewer), : const DeletePermanentActionButton(source: ActionSource.viewer),
const DeleteActionButton(source: ActionSource.viewer),
const MoveToLockFolderActionButton( const MoveToLockFolderActionButton(
source: ActionSource.viewer, source: ActionSource.viewer,
), ),

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
@ -80,6 +81,7 @@ class GeneralBottomSheet extends ConsumerWidget {
: const DeletePermanentActionButton( : const DeletePermanentActionButton(
source: ActionSource.timeline, source: ActionSource.timeline,
), ),
const DeleteActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal || multiselect.hasMerged) ...[ if (multiselect.hasLocal || multiselect.hasMerged) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),
], ],

View File

@ -230,6 +230,22 @@ class ActionNotifier extends Notifier<void> {
} }
} }
Future<ActionResult> trashRemoteAndDeleteLocal(ActionSource source) async {
final ids = _getOwnedRemoteIdsForSource(source);
final localIds = _getLocalIdsForSource(source);
try {
await _service.trashRemoteAndDeleteLocal(ids, localIds);
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> deleteRemoteAndLocal(ActionSource source) async { Future<ActionResult> deleteRemoteAndLocal(ActionSource source) async {
final ids = _getOwnedRemoteIdsForSource(source); final ids = _getOwnedRemoteIdsForSource(source);
final localIds = _getLocalIdsForSource(source); final localIds = _getLocalIdsForSource(source);
@ -249,8 +265,8 @@ class ActionNotifier extends Notifier<void> {
Future<ActionResult> deleteLocal(ActionSource source) async { Future<ActionResult> deleteLocal(ActionSource source) async {
final ids = _getLocalIdsForSource(source); final ids = _getLocalIdsForSource(source);
try { try {
await _service.deleteLocal(ids); final deletedCount = await _service.deleteLocal(ids);
return ActionResult(count: ids.length, success: true); return ActionResult(count: deletedCount, success: true);
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Failed to delete assets', error, stack); _logger.severe('Failed to delete assets', error, stack);
return ActionResult( return ActionResult(

View File

@ -132,6 +132,19 @@ class ActionService {
await _remoteAssetRepository.restoreTrash(ids); await _remoteAssetRepository.restoreTrash(ids);
} }
Future<void> trashRemoteAndDeleteLocal(List<String> remoteIds, List<String> localIds) async {
await _assetApiRepository.delete(remoteIds, false);
await _remoteAssetRepository.trash(remoteIds);
if (localIds.isNotEmpty) {
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
if (deletedIds.isNotEmpty) {
await _localAssetRepository.delete(deletedIds);
}
}
}
Future<void> deleteRemoteAndLocal( Future<void> deleteRemoteAndLocal(
List<String> remoteIds, List<String> remoteIds,
List<String> localIds, List<String> localIds,
@ -148,9 +161,14 @@ class ActionService {
} }
} }
Future<void> deleteLocal(List<String> localIds) async { Future<int> deleteLocal(List<String> localIds) async {
await _assetMediaRepository.deleteAll(localIds); final deletedIds = await _assetMediaRepository.deleteAll(localIds);
await _localAssetRepository.delete(localIds); if (deletedIds.isNotEmpty) {
await _localAssetRepository.delete(deletedIds);
return deletedIds.length;
}
return 0;
} }
Future<bool> editLocation( Future<bool> editLocation(