mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix(mobile): add restore option to trashed assets (#27442)
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
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/models/events.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.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 RestoreActionButton extends ConsumerWidget {
|
||||||
|
final ActionSource source;
|
||||||
|
final bool iconOnly;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const RestoreActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).restoreTrash(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
if (source == ActionSource.viewer) {
|
||||||
|
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
final successMessage = 'assets_restored_count'.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(
|
||||||
|
iconData: Icons.history_rounded,
|
||||||
|
label: 'restore'.t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
maxWidth: 100.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
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/domain/services/timeline.service.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/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/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/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/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@@ -33,19 +36,24 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
final serverInfo = ref.watch(serverInfoProvider);
|
final serverInfo = ref.watch(serverInfoProvider);
|
||||||
|
final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
|
||||||
|
|
||||||
final originalTheme = context.themeData;
|
final originalTheme = context.themeData;
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
if (isInTrash && isOwner && asset.hasRemote)
|
||||||
|
const RestoreActionButton(source: ActionSource.viewer)
|
||||||
|
else
|
||||||
|
const ShareActionButton(source: ActionSource.viewer),
|
||||||
|
|
||||||
if (!isInLockedView) ...[
|
if (!isInLockedView) ...[
|
||||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
if (!isInTrash) ...[
|
||||||
// edit sync was added in 2.6.0
|
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
||||||
if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
|
// edit sync was added in 2.6.0
|
||||||
const EditImageActionButton(),
|
if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
|
||||||
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
const EditImageActionButton(),
|
||||||
|
if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
|
||||||
|
],
|
||||||
if (isOwner) ...[
|
if (isOwner) ...[
|
||||||
asset.isLocalOnly
|
asset.isLocalOnly
|
||||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_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/set_album_cover.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_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';
|
||||||
@@ -81,6 +82,7 @@ enum ActionButtonType {
|
|||||||
moveToLockFolder,
|
moveToLockFolder,
|
||||||
removeFromLockFolder,
|
removeFromLockFolder,
|
||||||
removeFromAlbum,
|
removeFromAlbum,
|
||||||
|
restoreTrash,
|
||||||
trash,
|
trash,
|
||||||
deleteLocal,
|
deleteLocal,
|
||||||
deletePermanent,
|
deletePermanent,
|
||||||
@@ -112,7 +114,13 @@ enum ActionButtonType {
|
|||||||
context.isOwner && //
|
context.isOwner && //
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
context.asset.hasRemote && //
|
context.asset.hasRemote && //
|
||||||
context.isTrashEnabled,
|
context.isTrashEnabled && //
|
||||||
|
context.timelineOrigin != TimelineOrigin.trash,
|
||||||
|
ActionButtonType.restoreTrash =>
|
||||||
|
context.isOwner && //
|
||||||
|
!context.isInLockedView && //
|
||||||
|
context.asset.hasRemote && //
|
||||||
|
context.timelineOrigin == TimelineOrigin.trash,
|
||||||
ActionButtonType.deletePermanent =>
|
ActionButtonType.deletePermanent =>
|
||||||
context.isOwner && //
|
context.isOwner && //
|
||||||
context.asset.hasRemote && //
|
context.asset.hasRemote && //
|
||||||
@@ -201,6 +209,11 @@ enum ActionButtonType {
|
|||||||
),
|
),
|
||||||
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
|
ActionButtonType.restoreTrash => RestoreActionButton(
|
||||||
|
source: context.source,
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
),
|
||||||
ActionButtonType.deletePermanent => DeletePermanentActionButton(
|
ActionButtonType.deletePermanent => DeletePermanentActionButton(
|
||||||
source: context.source,
|
source: context.source,
|
||||||
iconOnly: iconOnly,
|
iconOnly: iconOnly,
|
||||||
@@ -292,6 +305,7 @@ enum ActionButtonType {
|
|||||||
ActionButtonType.moveToLockFolder => 10,
|
ActionButtonType.moveToLockFolder => 10,
|
||||||
ActionButtonType.deleteLocal => 10,
|
ActionButtonType.deleteLocal => 10,
|
||||||
ActionButtonType.delete => 10,
|
ActionButtonType.delete => 10,
|
||||||
|
ActionButtonType.restoreTrash => 10,
|
||||||
// 90: advancedInfo
|
// 90: advancedInfo
|
||||||
ActionButtonType.advancedInfo => 90,
|
ActionButtonType.advancedInfo => 90,
|
||||||
// 1: others
|
// 1: others
|
||||||
@@ -309,6 +323,7 @@ class ActionButtonBuilder {
|
|||||||
ActionButtonType.delete,
|
ActionButtonType.delete,
|
||||||
ActionButtonType.archive,
|
ActionButtonType.archive,
|
||||||
ActionButtonType.unarchive,
|
ActionButtonType.unarchive,
|
||||||
|
ActionButtonType.restoreTrash,
|
||||||
};
|
};
|
||||||
|
|
||||||
static List<Widget> build(ActionButtonContext context) {
|
static List<Widget> build(ActionButtonContext context) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:immich_mobile/constants/enums.dart';
|
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/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
LocalAsset createLocalAsset({
|
LocalAsset createLocalAsset({
|
||||||
@@ -460,6 +461,44 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('restoreTrash button', () {
|
||||||
|
test('should show when owner, not locked, has remote, and is in trash timeline', () {
|
||||||
|
final remoteAsset = createRemoteAsset();
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: remoteAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
timelineOrigin: TimelineOrigin.trash,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ActionButtonType.restoreTrash.shouldShow(context), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not show when not in trash timeline', () {
|
||||||
|
final remoteAsset = createRemoteAsset();
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: remoteAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: false,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
timelineOrigin: TimelineOrigin.main,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ActionButtonType.restoreTrash.shouldShow(context), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('deletePermanent button', () {
|
group('deletePermanent button', () {
|
||||||
test('should show when owner, not locked, has remote, and trash disabled', () {
|
test('should show when owner, not locked, has remote, and trash disabled', () {
|
||||||
final remoteAsset = createRemoteAsset();
|
final remoteAsset = createRemoteAsset();
|
||||||
|
|||||||
Reference in New Issue
Block a user