mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
chore: delete action button (#20261)
This commit is contained in:
parent
b14c768208
commit
3a5d82f790
@ -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",
|
||||||
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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()},
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user