mirror of
https://github.com/immich-app/immich.git
synced 2026-07-01 10:35:13 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7130553634 | |||
| d26f6c0665 |
@@ -104,4 +104,13 @@ class AssetService {
|
||||
await _remoteRepository.unStack(stackIds);
|
||||
await _apiRepository.unStack(stackIds);
|
||||
}
|
||||
|
||||
Future<void> updateVisibility(List<String> remoteIds, AssetVisibility visibility) async {
|
||||
if (remoteIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _apiRepository.updateVisibility(remoteIds, visibility);
|
||||
await _remoteRepository.updateVisibility(remoteIds, visibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||
|
||||
class ActionScope {
|
||||
final BuildContext context;
|
||||
@@ -28,5 +29,5 @@ abstract class AssetAction<T extends BaseAsset> extends BaseAction {
|
||||
|
||||
const AssetAction({required this.assets});
|
||||
|
||||
Iterable<T> filter(ActionScope scope) => assets.whereType<T>();
|
||||
AssetFilter<T> filter(ActionScope scope) => .new(assets.whereType<T>());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/utils/asset_filter.dart';
|
||||
|
||||
class ArchiveAction extends AssetAction<RemoteAsset> {
|
||||
final bool archive;
|
||||
|
||||
ArchiveAction({required super.assets})
|
||||
: archive = assets.any((asset) => asset is RemoteAsset && asset.visibility != .archive);
|
||||
|
||||
@override
|
||||
IconData get icon => archive ? Icons.archive_outlined : Icons.unarchive_outlined;
|
||||
|
||||
@override
|
||||
String label(ActionScope scope) => archive ? scope.context.t.archive : scope.context.t.unarchive;
|
||||
|
||||
@override
|
||||
AssetFilter<RemoteAsset> filter(ActionScope scope) =>
|
||||
.new(assets).owned(scope.authUser.id).archived(isArchived: !archive);
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => !scope.ref.watch(inLockedViewProvider) && filter(scope).isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<void> onAction(ActionScope scope) async {
|
||||
final ActionScope(:ref, :context) = scope;
|
||||
final assets = filter(scope).map((asset) => asset.id).toList(growable: false);
|
||||
|
||||
await ref.read(assetServiceProvider).updateVisibility(assets, archive ? .archive : .timeline);
|
||||
final message = archive
|
||||
? context.t.archive_action_prompt(count: assets.length)
|
||||
: context.t.unarchive_action_prompt(count: assets.length);
|
||||
ref.read(toastRepositoryProvider).success(message);
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ class FavoriteAction extends AssetAction<RemoteAsset> {
|
||||
String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite;
|
||||
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) =>
|
||||
AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite);
|
||||
AssetFilter<RemoteAsset> filter(ActionScope scope) =>
|
||||
.new(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite);
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
|
||||
|
||||
@@ -16,7 +16,7 @@ class RestoreAction extends AssetAction<RemoteAsset> {
|
||||
String label(ActionScope scope) => scope.context.t.restore;
|
||||
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) => AssetFilter(assets).owned(scope.authUser.id).trashed();
|
||||
AssetFilter<RemoteAsset> filter(ActionScope scope) => .new(assets).owned(scope.authUser.id).trashed();
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => filter(scope).isNotEmpty;
|
||||
|
||||
@@ -18,7 +18,7 @@ class StackAction extends AssetAction<RemoteAsset> {
|
||||
String label(ActionScope scope) => stack ? scope.context.t.stack : scope.context.t.unstack;
|
||||
|
||||
@override
|
||||
Iterable<RemoteAsset> filter(ActionScope scope) => AssetFilter(assets).owned(scope.authUser.id);
|
||||
AssetFilter<RemoteAsset> filter(ActionScope scope) => .new(assets).owned(scope.authUser.id);
|
||||
|
||||
@override
|
||||
bool isVisible(ActionScope scope) => stack ? filter(scope).length > 1 : filter(scope).isNotEmpty;
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
import 'package:immich_mobile/constants/enums.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/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
||||
enum AddToMenuItem { album, lockedFolder }
|
||||
|
||||
class AddActionButton extends ConsumerStatefulWidget {
|
||||
const AddActionButton({super.key, this.originalTheme});
|
||||
@@ -37,12 +35,6 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
case AddToMenuItem.album:
|
||||
_openAlbumSelector();
|
||||
break;
|
||||
case AddToMenuItem.archive:
|
||||
performArchiveAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
case AddToMenuItem.unarchive:
|
||||
performUnArchiveAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
case AddToMenuItem.lockedFolder:
|
||||
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
@@ -57,11 +49,6 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
|
||||
final user = ref.read(currentUserProvider);
|
||||
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final hasRemote = asset is RemoteAsset;
|
||||
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
|
||||
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
|
||||
|
||||
return [
|
||||
Padding(
|
||||
@@ -81,20 +68,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||
),
|
||||
if (showArchive)
|
||||
BaseActionButton(
|
||||
iconData: Icons.archive_outlined,
|
||||
label: "archive".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
|
||||
),
|
||||
if (showUnarchive)
|
||||
BaseActionButton(
|
||||
iconData: Icons.unarchive_outlined,
|
||||
label: "unarchive".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
|
||||
),
|
||||
ActionMenuItemWidget(action: ArchiveAction(assets: [asset])),
|
||||
BaseActionButton(
|
||||
iconData: Icons.lock_outline,
|
||||
label: "locked_folder".tr(),
|
||||
@@ -184,7 +158,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
|
||||
final themeData = widget.originalTheme ?? context.themeData;
|
||||
|
||||
return MenuAnchor(
|
||||
return ImmichMenu(
|
||||
consumeOutsideTap: true,
|
||||
style: MenuStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
|
||||
@@ -195,7 +169,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||
),
|
||||
menuChildren: widget.originalTheme != null
|
||||
children: widget.originalTheme != null
|
||||
? [
|
||||
Theme(
|
||||
data: widget.originalTheme!,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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';
|
||||
|
||||
// used to allow performing archive action from different sources (without duplicating code)
|
||||
Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source == ActionSource.viewer) {
|
||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||
}
|
||||
|
||||
final result = await ref.read(actionProvider.notifier).archive(source);
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'archive_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArchiveActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const ArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
await performArchiveAction(context, ref, source: source);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.archive_outlined,
|
||||
label: "to_archive".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// dart
|
||||
// File: `lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart`
|
||||
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/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';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
|
||||
// used to allow performing unarchive action from different sources (without duplicating code)
|
||||
Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source == ActionSource.viewer) {
|
||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||
}
|
||||
|
||||
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'unarchive_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnArchiveActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const UnArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
await performUnArchiveAction(context, ref, source: source);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.unarchive_outlined,
|
||||
label: "unarchive".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
@@ -16,7 +17,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
@@ -76,7 +76,7 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [FavoriteAction(assets: assets), StackAction(assets: assets)];
|
||||
final actions = [FavoriteAction(assets: assets), ArchiveAction(assets: assets), StackAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
@@ -87,7 +87,6 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
|
||||
@@ -5,10 +5,10 @@ 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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_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/download_action_button.widget.dart';
|
||||
@@ -67,7 +67,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [FavoriteAction(assets: assets), StackAction(assets: assets)];
|
||||
final actions = [FavoriteAction(assets: assets), ArchiveAction(assets: assets), StackAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.4,
|
||||
@@ -78,7 +78,6 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_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';
|
||||
@@ -83,7 +83,12 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [AssetDebugAction(assets: assets), FavoriteAction(assets: assets), StackAction(assets: assets)];
|
||||
final actions = [
|
||||
AssetDebugAction(assets: assets),
|
||||
FavoriteAction(assets: assets),
|
||||
ArchiveAction(assets: assets),
|
||||
StackAction(assets: assets),
|
||||
];
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
@@ -100,7 +105,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -4,10 +4,10 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_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/download_action_button.widget.dart';
|
||||
@@ -85,7 +85,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
}
|
||||
|
||||
final assets = multiselect.selectedAssets.toList(growable: false);
|
||||
final actions = [FavoriteAction(assets: assets), StackAction(assets: assets)];
|
||||
final actions = [FavoriteAction(assets: assets), ArchiveAction(assets: assets), StackAction(assets: assets)];
|
||||
|
||||
return BaseBottomSheet(
|
||||
controller: sheetController,
|
||||
@@ -99,7 +99,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
|
||||
if (ownsAlbum) ...[
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
],
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -156,28 +156,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> archive(ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
await _service.archive(ids);
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to archive assets', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> unArchive(ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
await _service.unArchive(ids);
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to unarchive assets', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
||||
|
||||
@@ -68,16 +68,6 @@ class ActionService {
|
||||
unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds)));
|
||||
}
|
||||
|
||||
Future<void> archive(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .archive);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive);
|
||||
}
|
||||
|
||||
Future<void> unArchive(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .timeline);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
Future<void> moveToLockFolder(List<String> remoteIds, List<String> localIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .locked);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
||||
|
||||
@@ -8,10 +8,10 @@ import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||
@@ -30,7 +30,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@@ -200,12 +199,8 @@ enum ActionButtonType {
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.unarchive => UnArchiveActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.archive ||
|
||||
ActionButtonType.unarchive => ActionMenuItemWidget(action: ArchiveAction(assets: [context.asset])),
|
||||
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.restoreTrash => ActionMenuItemWidget(action: RestoreAction(assets: [context.asset])),
|
||||
|
||||
@@ -94,6 +94,7 @@ class ServiceMocks {
|
||||
when(asset.stack).thenAnswer((_) async {});
|
||||
when(asset.unstack).thenAnswer((_) async {});
|
||||
when(asset.restoreTrash).thenAnswer((_) async {});
|
||||
when(asset.updateVisibility).thenAnswer((_) async {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +102,7 @@ void _registerFallbacks() {
|
||||
registerFallbackValue(LocalAlbumFactory.create());
|
||||
registerFallbackValue(LocalAssetFactory.create());
|
||||
registerFallbackValue(Uint8List(0));
|
||||
registerFallbackValue(AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
extension type const Stub<T extends Mock>(T mockedClass) {
|
||||
@@ -181,6 +183,9 @@ extension type const AssetServiceStub(MockAssetService service) implements Stub<
|
||||
|
||||
Future<void> Function() get restoreTrash =>
|
||||
() => service.restoreTrash(any());
|
||||
|
||||
Future<void> Function() get updateVisibility =>
|
||||
() => service.updateVisibility(any(), any());
|
||||
}
|
||||
|
||||
extension type const NativeSyncApiStub(MockNativeSyncApi api) implements Stub<MockNativeSyncApi> {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../domain/service.mock.dart';
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
late MockAssetService assetService;
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
assetService = context.service.asset.service;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
RemoteAsset owned({AssetVisibility visibility = AssetVisibility.timeline}) =>
|
||||
RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility);
|
||||
|
||||
group('ArchiveAction', () {
|
||||
testWidgets('archives the eligible owned assets', (tester) async {
|
||||
final asset = owned();
|
||||
|
||||
await tester.pumpTestAction(context, ArchiveAction(assets: [asset]));
|
||||
|
||||
verify(() => assetService.updateVisibility([asset.id], AssetVisibility.archive)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('unarchive the eligible owned assets', (tester) async {
|
||||
final asset = owned(visibility: AssetVisibility.archive);
|
||||
|
||||
await tester.pumpTestAction(context, ArchiveAction(assets: [asset]));
|
||||
|
||||
verify(() => assetService.updateVisibility([asset.id], AssetVisibility.timeline)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('ignores assets owned by someone else', (tester) async {
|
||||
final mine = owned();
|
||||
final theirs = RemoteAssetFactory.create();
|
||||
|
||||
await tester.pumpTestAction(context, ArchiveAction(assets: [mine, theirs]));
|
||||
|
||||
verify(() => assetService.updateVisibility([mine.id], AssetVisibility.archive)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
||||
final first = owned();
|
||||
final second = owned();
|
||||
|
||||
await tester.pumpTestAction(context, ArchiveAction(assets: [first, second]));
|
||||
|
||||
verify(() => assetService.updateVisibility([first.id, second.id], AssetVisibility.archive)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('skips owned assets already in the target state', (tester) async {
|
||||
final stale = owned();
|
||||
final alreadyArchived = owned(visibility: AssetVisibility.archive);
|
||||
|
||||
await tester.pumpTestAction(context, ArchiveAction(assets: [stale, alreadyArchived]));
|
||||
|
||||
verify(() => assetService.updateVisibility([stale.id], AssetVisibility.archive)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('reports the archived count through the toast repository', (tester) async {
|
||||
final toast = context.repository.toast;
|
||||
|
||||
await tester.pumpTestAction(context, ArchiveAction(assets: [owned(), owned()]));
|
||||
|
||||
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||
expect(message, StaticTranslations.instance.archive_action_prompt(count: 2));
|
||||
});
|
||||
|
||||
testWidgets('reports the unarchive count through the toast repository', (tester) async {
|
||||
final toast = context.repository.toast;
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
ArchiveAction(
|
||||
assets: [
|
||||
owned(visibility: AssetVisibility.archive),
|
||||
owned(visibility: AssetVisibility.archive),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||
expect(message, StaticTranslations.instance.unarchive_action_prompt(count: 2));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user