fix(mobile): use correct delete action (#26575)

fix(mobile): use correct delete for trashed assets

When viewing a trashed asset, the viewer bottom bar now shows the permanent delete button instead of the trash button, which had no effect on already-trashed assets.
This commit is contained in:
Marius
2026-05-14 13:57:19 +02:00
committed by GitHub
parent 84a2b7a3c8
commit 37cc028868
6 changed files with 71 additions and 10 deletions
@@ -11,6 +11,7 @@ class RemoteAsset extends BaseAsset {
final String ownerId; final String ownerId;
final String? stackId; final String? stackId;
final DateTime? uploadedAt; final DateTime? uploadedAt;
final DateTime? deletedAt;
const RemoteAsset({ const RemoteAsset({
required this.id, required this.id,
@@ -31,6 +32,7 @@ class RemoteAsset extends BaseAsset {
super.livePhotoVideoId, super.livePhotoVideoId,
this.stackId, this.stackId,
required super.isEdited, required super.isEdited,
this.deletedAt,
}) : localAssetId = localId; }) : localAssetId = localId;
@override @override
@@ -48,6 +50,8 @@ class RemoteAsset extends BaseAsset {
@override @override
bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage; bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage;
bool get isTrashed => deletedAt != null;
@override @override
String toString() { String toString() {
return '''Asset { return '''Asset {
@@ -86,7 +90,8 @@ class RemoteAsset extends BaseAsset {
thumbHash == other.thumbHash && thumbHash == other.thumbHash &&
visibility == other.visibility && visibility == other.visibility &&
stackId == other.stackId && stackId == other.stackId &&
uploadedAt == other.uploadedAt; uploadedAt == other.uploadedAt &&
deletedAt == other.deletedAt;
} }
@override @override
@@ -98,7 +103,8 @@ class RemoteAsset extends BaseAsset {
thumbHash.hashCode ^ thumbHash.hashCode ^
visibility.hashCode ^ visibility.hashCode ^
stackId.hashCode ^ stackId.hashCode ^
uploadedAt.hashCode; uploadedAt.hashCode ^
deletedAt.hashCode;
RemoteAsset copyWith({ RemoteAsset copyWith({
String? id, String? id,
@@ -119,6 +125,7 @@ class RemoteAsset extends BaseAsset {
String? livePhotoVideoId, String? livePhotoVideoId,
String? stackId, String? stackId,
bool? isEdited, bool? isEdited,
DateTime? deletedAt,
}) { }) {
return RemoteAsset( return RemoteAsset(
id: id ?? this.id, id: id ?? this.id,
@@ -139,6 +146,7 @@ class RemoteAsset extends BaseAsset {
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
stackId: stackId ?? this.stackId, stackId: stackId ?? this.stackId,
isEdited: isEdited ?? this.isEdited, isEdited: isEdited ?? this.isEdited,
deletedAt: deletedAt ?? this.deletedAt,
); );
} }
} }
@@ -156,6 +164,7 @@ class RemoteAssetExif extends RemoteAsset {
required super.createdAt, required super.createdAt,
required super.updatedAt, required super.updatedAt,
super.uploadedAt, super.uploadedAt,
super.deletedAt,
super.width, super.width,
super.height, super.height,
super.durationMs, super.durationMs,
@@ -193,6 +202,7 @@ class RemoteAssetExif extends RemoteAsset {
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
DateTime? uploadedAt, DateTime? uploadedAt,
DateTime? deletedAt,
int? width, int? width,
int? height, int? height,
int? durationMs, int? durationMs,
@@ -214,6 +224,7 @@ class RemoteAssetExif extends RemoteAsset {
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt, uploadedAt: uploadedAt ?? this.uploadedAt,
deletedAt: deletedAt ?? this.deletedAt,
width: width ?? this.width, width: width ?? this.width,
height: height ?? this.height, height: height ?? this.height,
durationMs: durationMs ?? this.durationMs, durationMs: durationMs ?? this.durationMs,
@@ -74,5 +74,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
localId: localId, localId: localId,
stackId: stackId, stackId: stackId,
isEdited: isEdited, isEdited: isEdited,
deletedAt: deletedAt,
); );
} }
@@ -18,8 +18,15 @@ class DeletePermanentActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly; final bool iconOnly;
final bool menuItem; final bool menuItem;
final bool useShortLabel;
const DeletePermanentActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); const DeletePermanentActionButton({
super.key,
required this.source,
this.iconOnly = false,
this.menuItem = false,
this.useShortLabel = false,
});
void _onTap(BuildContext context, WidgetRef ref) async { void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) { if (!context.mounted) {
@@ -64,7 +71,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_permanently".t(context: context), label: useShortLabel ? "delete".t(context: context) : "delete_permanently".t(context: context),
iconOnly: iconOnly, iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
@@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_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_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/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/restore_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';
@@ -55,9 +56,12 @@ class ViewerBottomBar extends ConsumerWidget {
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
], ],
if (isOwner) ...[ if (isOwner) ...[
asset.isLocalOnly if (asset.isLocalOnly)
? const DeleteLocalActionButton(source: ActionSource.viewer) const DeleteLocalActionButton(source: ActionSource.viewer)
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true), else if (asset.isTrashed)
const DeletePermanentActionButton(source: ActionSource.viewer, useShortLabel: true)
else
const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
], ],
], ],
]; ];
+3 -3
View File
@@ -123,9 +123,8 @@ enum ActionButtonType {
context.timelineOrigin == TimelineOrigin.trash, context.timelineOrigin == TimelineOrigin.trash,
ActionButtonType.deletePermanent => ActionButtonType.deletePermanent =>
context.isOwner && // context.isOwner && //
context.asset.hasRemote && // context.asset.hasRemote && //
!context.isTrashEnabled || (!context.isTrashEnabled || context.timelineOrigin == TimelineOrigin.trash || context.isInLockedView),
context.isInLockedView,
ActionButtonType.delete => ActionButtonType.delete =>
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
@@ -324,6 +323,7 @@ class ActionButtonBuilder {
ActionButtonType.archive, ActionButtonType.archive,
ActionButtonType.unarchive, ActionButtonType.unarchive,
ActionButtonType.restoreTrash, ActionButtonType.restoreTrash,
ActionButtonType.deletePermanent,
}; };
static List<Widget> build(ActionButtonContext context) { static List<Widget> build(ActionButtonContext context) {
@@ -38,6 +38,7 @@ RemoteAsset createRemoteAsset({
DateTime? updatedAt, DateTime? updatedAt,
DateTime? uploadedAt, DateTime? uploadedAt,
bool isFavorite = false, bool isFavorite = false,
DateTime? deletedAt,
}) { }) {
return RemoteAsset( return RemoteAsset(
id: 'remote-id', id: 'remote-id',
@@ -51,6 +52,7 @@ RemoteAsset createRemoteAsset({
uploadedAt: uploadedAt ?? DateTime.now(), uploadedAt: uploadedAt ?? DateTime.now(),
isFavorite: isFavorite, isFavorite: isFavorite,
isEdited: false, isEdited: false,
deletedAt: deletedAt,
); );
} }
@@ -459,6 +461,24 @@ void main() {
expect(ActionButtonType.trash.shouldShow(context), isFalse); expect(ActionButtonType.trash.shouldShow(context), isFalse);
}); });
test('should not show when asset is already trashed', () {
final remoteAsset = createRemoteAsset(deletedAt: DateTime(2024));
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.viewer,
timelineOrigin: TimelineOrigin.trash,
);
expect(ActionButtonType.trash.shouldShow(context), isFalse);
});
}); });
group('restoreTrash button', () { group('restoreTrash button', () {
@@ -533,6 +553,24 @@ void main() {
expect(ActionButtonType.deletePermanent.shouldShow(context), isFalse); expect(ActionButtonType.deletePermanent.shouldShow(context), isFalse);
}); });
test('should show when asset is trashed even with trash enabled', () {
final remoteAsset = createRemoteAsset(deletedAt: DateTime(2024));
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.viewer,
timelineOrigin: TimelineOrigin.trash,
);
expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue);
});
}); });
group('delete button', () { group('delete button', () {