mirror of
https://github.com/immich-app/immich.git
synced 2026-07-10 14:14:00 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cf19f19df | |||
| cc5ac5cca5 | |||
| b033a18ce0 |
@@ -56,6 +56,8 @@ class RemoteAsset extends BaseAsset {
|
||||
|
||||
bool get isArchived => visibility == .archive;
|
||||
|
||||
bool get isLocked => visibility == .locked;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''Asset {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/presentation/actions/action.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/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
|
||||
class AssetActions {
|
||||
@@ -10,13 +11,21 @@ class AssetActions {
|
||||
final FavoriteAction favorite;
|
||||
final ArchiveAction archive;
|
||||
final StackAction stack;
|
||||
final LockAction lock;
|
||||
|
||||
const AssetActions({required this.debug, required this.favorite, required this.archive, required this.stack});
|
||||
const AssetActions({
|
||||
required this.debug,
|
||||
required this.favorite,
|
||||
required this.archive,
|
||||
required this.stack,
|
||||
required this.lock,
|
||||
});
|
||||
|
||||
static AssetActions from(ActionScope scope, List<BaseAsset> assets) => .new(
|
||||
debug: AssetDebugAction(assets: assets, scope: scope),
|
||||
favorite: FavoriteAction(assets: assets, scope: scope),
|
||||
archive: ArchiveAction(assets: assets, scope: scope),
|
||||
stack: StackAction(assets: assets, scope: scope),
|
||||
lock: LockAction(assets: assets, scope: scope),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||
|
||||
class CastAction extends BaseAction {
|
||||
const CastAction._({required super.scope, required super.icon, required super.label});
|
||||
|
||||
factory CastAction({required ActionScope scope}) {
|
||||
final casting = scope.ref.watch(castProvider.select((state) => state.isCasting));
|
||||
return CastAction._(
|
||||
scope: scope,
|
||||
icon: casting ? Icons.cast_connected_rounded : Icons.cast_rounded,
|
||||
label: scope.context.t.cast,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onAction() async {
|
||||
unawaited(showDialog(context: scope.context, builder: (_) => const CastDialog()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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/utils/asset_filter.dart';
|
||||
|
||||
class LockAction extends BaseAction {
|
||||
final List<String> assetIds;
|
||||
final bool lock;
|
||||
|
||||
const LockAction._({
|
||||
required this.assetIds,
|
||||
required this.lock,
|
||||
required super.scope,
|
||||
required super.icon,
|
||||
required super.label,
|
||||
super.isVisible,
|
||||
});
|
||||
|
||||
factory LockAction({required Iterable<BaseAsset> assets, required ActionScope scope}) {
|
||||
final ownedAssets = AssetFilter(assets).owned(scope.authUser.id);
|
||||
final lock = ownedAssets.locked(isLocked: false).isNotEmpty;
|
||||
final assetIds = ownedAssets.locked(isLocked: !lock).map((asset) => asset.id).toList(growable: false);
|
||||
|
||||
return LockAction._(
|
||||
assetIds: assetIds,
|
||||
lock: lock,
|
||||
scope: scope,
|
||||
icon: lock ? Icons.lock_rounded : Icons.lock_open_rounded,
|
||||
label: lock ? scope.context.t.move_to_locked_folder : scope.context.t.remove_from_locked_folder,
|
||||
isVisible: assetIds.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onAction() async {
|
||||
final ActionScope(:ref, :context) = scope;
|
||||
|
||||
await ref.read(assetServiceProvider).update(assetIds, visibility: .some(lock ? .locked : .timeline));
|
||||
final message = lock
|
||||
? scope.context.t.move_to_lock_folder_action_prompt(count: assetIds.length)
|
||||
: scope.context.t.remove_from_lock_folder_action_prompt(count: assetIds.length);
|
||||
ref.read(toastRepositoryProvider).success(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OpenInBrowserAction extends BaseAction {
|
||||
final String remoteId;
|
||||
final TimelineOrigin origin;
|
||||
|
||||
OpenInBrowserAction({required this.remoteId, required this.origin, required super.scope})
|
||||
: super(icon: Icons.open_in_browser, label: scope.context.t.open_in_browser);
|
||||
|
||||
@override
|
||||
Future<void> onAction() async {
|
||||
final serverEndpoint = Store.get(.serverEndpoint).replaceFirst('/api', '');
|
||||
|
||||
final originPath = switch (origin) {
|
||||
.favorite => '/favorites',
|
||||
.trash => '/trash',
|
||||
.archive => '/archive',
|
||||
_ => '',
|
||||
};
|
||||
|
||||
final url = Uri.parse('$serverEndpoint$originPath/photos/$remoteId');
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: .externalApplication);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
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/routing/router.dart';
|
||||
|
||||
class SetProfilePictureAction extends BaseAction {
|
||||
final BaseAsset asset;
|
||||
|
||||
SetProfilePictureAction({required this.asset, required super.scope})
|
||||
: super(icon: Icons.account_circle_outlined, label: scope.context.t.set_as_profile_picture);
|
||||
|
||||
@override
|
||||
Future<void> onAction() async => unawaited(scope.context.pushRoute(ProfilePictureCropRoute(asset: asset)));
|
||||
}
|
||||
+8
-23
@@ -2,26 +2,23 @@ import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
class SimilarPhotosAction extends BaseAction {
|
||||
final String assetId;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false});
|
||||
SimilarPhotosAction({required this.assetId, required super.scope})
|
||||
: super(icon: Icons.compare, label: scope.context.t.view_similar_photos);
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
@override
|
||||
Future<void> onAction() async {
|
||||
final ActionScope(:context, :ref) = scope;
|
||||
|
||||
ref.invalidate(assetViewerProvider);
|
||||
ref.invalidate(paginatedSearchProvider);
|
||||
@@ -43,16 +40,4 @@ class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
|
||||
unawaited(context.navigateTo(const DriftSearchRoute()));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.compare,
|
||||
label: "view_similar_photos".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
maxWidth: 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SlideshowAction extends BaseAction {
|
||||
SlideshowAction({required super.scope}) : super(icon: Icons.slideshow, label: scope.context.t.slideshow);
|
||||
|
||||
@override
|
||||
Future<void> onAction() async {
|
||||
final ActionScope(:context, :ref) = scope;
|
||||
unawaited(context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))));
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.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/lock.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';
|
||||
@@ -19,7 +19,7 @@ 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, lockedFolder }
|
||||
enum AddToMenuItem { album }
|
||||
|
||||
class AddActionButton extends ConsumerStatefulWidget {
|
||||
const AddActionButton({super.key, this.originalTheme});
|
||||
@@ -36,9 +36,6 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
case AddToMenuItem.album:
|
||||
_openAlbumSelector();
|
||||
break;
|
||||
case AddToMenuItem.lockedFolder:
|
||||
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +70,8 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
ActionMenuItemWidget(
|
||||
action: ArchiveAction(assets: [asset], scope: scope),
|
||||
),
|
||||
BaseActionButton(
|
||||
iconData: Icons.lock_outline,
|
||||
label: "locked_folder".tr(),
|
||||
menuItem: true,
|
||||
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
|
||||
ActionMenuItemWidget(
|
||||
action: LockAction(assets: [asset], scope: scope),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||
|
||||
class CastActionButton extends ConsumerWidget {
|
||||
const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
|
||||
return BaseActionButton(
|
||||
iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded,
|
||||
iconColor: isCasting ? context.primaryColor : null, // null = default color
|
||||
label: "cast".t(context: context),
|
||||
onPressed: () {
|
||||
showDialog(context: context, builder: (context) => const CastDialog());
|
||||
},
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
);
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +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';
|
||||
|
||||
// Reusable helper: move to locked folder from any source (e.g called from menu)
|
||||
Future<void> performMoveToLockFolderAction(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).moveToLockFolder(source);
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'move_to_lock_folder_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 MoveToLockFolderActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const MoveToLockFolderActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Future<void> _onTap(BuildContext context, WidgetRef ref) async {
|
||||
await performMoveToLockFolderAction(context, ref, source: source);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
maxWidth: 115.0,
|
||||
iconData: Icons.lock_outline_rounded,
|
||||
label: "move_to_locked_folder".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OpenInBrowserActionButton extends ConsumerWidget {
|
||||
final String remoteId;
|
||||
final TimelineOrigin origin;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const OpenInBrowserActionButton({
|
||||
super.key,
|
||||
required this.remoteId,
|
||||
required this.origin,
|
||||
this.iconOnly = false,
|
||||
this.menuItem = false,
|
||||
});
|
||||
|
||||
void _onTap() async {
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', '');
|
||||
|
||||
String originPath = '';
|
||||
switch (origin) {
|
||||
case TimelineOrigin.favorite:
|
||||
originPath = '/favorites';
|
||||
break;
|
||||
case TimelineOrigin.trash:
|
||||
originPath = '/trash';
|
||||
break;
|
||||
case TimelineOrigin.archive:
|
||||
originPath = '/archive';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
final url = '$serverEndpoint$originPath/photos/$remoteId';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
label: 'open_in_browser'.t(context: context),
|
||||
iconData: Icons.open_in_browser,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: _onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +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/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 RemoveFromLockFolderActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const RemoveFromLockFolderActionButton({
|
||||
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).removeFromLockFolder(source);
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'remove_from_lock_folder_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: 100.0,
|
||||
iconData: Icons.lock_open_rounded,
|
||||
label: "remove_from_locked_folder".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SetProfilePictureActionButton extends ConsumerWidget {
|
||||
final BaseAsset asset;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.pushRoute(ProfilePictureCropRoute(asset: asset));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.account_circle_outlined,
|
||||
label: "set_as_profile_picture".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context),
|
||||
maxWidth: 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/timeline.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SlideshowActionButton extends ConsumerWidget {
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.slideshow,
|
||||
label: "slideshow".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
maxWidth: 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,9 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
|
||||
final assets = [asset];
|
||||
final scope = ActionScope.from(context, ref);
|
||||
final restore = RestoreAction(assets: assets, scope: scope);
|
||||
final actions = <Widget>[
|
||||
ActionColumnButtonWidget(
|
||||
action: RestoreAction(assets: assets, scope: scope),
|
||||
),
|
||||
if (restore.isVisible) ActionColumnButtonWidget(action: restore),
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
|
||||
if (!isInLockedView) ...[
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_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/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';
|
||||
@@ -90,6 +89,7 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
actions.favorite,
|
||||
actions.archive,
|
||||
actions.stack,
|
||||
actions.lock,
|
||||
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
@@ -97,7 +97,6 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_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/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';
|
||||
@@ -80,6 +79,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
actions.favorite,
|
||||
actions.archive,
|
||||
actions.stack,
|
||||
actions.lock,
|
||||
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
@@ -87,7 +87,6 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_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/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';
|
||||
@@ -95,6 +94,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
actions.favorite,
|
||||
actions.archive,
|
||||
actions.stack,
|
||||
actions.lock,
|
||||
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
@@ -106,7 +106,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
if (multiselect.onlyLocal || multiselect.hasMerged)
|
||||
|
||||
+16
-6
@@ -1,26 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/timeline.action.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';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
|
||||
class LockedFolderBottomSheet extends ConsumerWidget {
|
||||
const LockedFolderBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const BaseBottomSheet(
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
final scope = ActionScope.from(context, ref);
|
||||
return BaseBottomSheet(
|
||||
initialChildSize: 0.25,
|
||||
maxChildSize: 0.4,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
DownloadActionButton(source: ActionSource.timeline),
|
||||
DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
ActionColumnButtonWidget(
|
||||
action: TimelineAction(
|
||||
action: LockAction(assets: multiselect.selectedAssets.toList(growable: false), scope: scope),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_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/action_buttons/remove_from_album_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/share_action_button.widget.dart';
|
||||
@@ -102,6 +101,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
actions.favorite,
|
||||
actions.archive,
|
||||
actions.stack,
|
||||
actions.lock,
|
||||
].map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))),
|
||||
],
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
@@ -111,7 +111,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
: const DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
],
|
||||
if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -8,20 +7,17 @@ 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/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/action.service.dart';
|
||||
import 'package:immich_mobile/services/download.service.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
@@ -49,7 +45,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
late ActionService _service;
|
||||
late ForegroundUploadService _foregroundUploadService;
|
||||
late DownloadService _downloadService;
|
||||
late AssetService _assetService;
|
||||
|
||||
ActionNotifier() : super();
|
||||
|
||||
@@ -57,7 +52,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
void build() {
|
||||
_foregroundUploadService = ref.watch(foregroundUploadServiceProvider);
|
||||
_service = ref.watch(actionServiceProvider);
|
||||
_assetService = ref.watch(assetServiceProvider);
|
||||
_downloadService = ref.watch(downloadServiceProvider);
|
||||
_downloadService.onImageDownloadStatus = _downloadImageCallback;
|
||||
_downloadService.onVideoDownloadStatus = _downloadVideoCallback;
|
||||
@@ -110,21 +104,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
return _getAssets(source).whereType<RemoteAsset>().ownedAssets(ownerId).toIds().toList(growable: false);
|
||||
}
|
||||
|
||||
List<RemoteAsset> _getOwnedRemoteAssetsForSource(ActionSource source) {
|
||||
final ownerId = ref.read(currentUserProvider)?.id;
|
||||
return _getIdsForSource<RemoteAsset>(source).ownedAssets(ownerId).toList();
|
||||
}
|
||||
|
||||
Iterable<T> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
|
||||
final Set<BaseAsset> assets = _getAssets(source);
|
||||
return switch (T) {
|
||||
const (RemoteAsset) => assets.whereType<RemoteAsset>(),
|
||||
const (LocalAsset) => assets.whereType<LocalAsset>(),
|
||||
_ => const [],
|
||||
}
|
||||
as Iterable<T>;
|
||||
}
|
||||
|
||||
Set<BaseAsset> _getAssets(ActionSource source) {
|
||||
return switch (source) {
|
||||
ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets,
|
||||
@@ -135,16 +114,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
};
|
||||
}
|
||||
|
||||
Future<ActionResult> troubleshoot(ActionSource source, BuildContext context) async {
|
||||
final assets = _getAssets(source);
|
||||
if (assets.length > 1) {
|
||||
return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets');
|
||||
}
|
||||
unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
|
||||
|
||||
return ActionResult(count: assets.length, success: true);
|
||||
}
|
||||
|
||||
Future<ActionResult> shareLink(ActionSource source, BuildContext context) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
@@ -156,29 +125,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
||||
try {
|
||||
await _service.moveToLockFolder(ids, localIds);
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to move assets to lock folder', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> removeFromLockFolder(ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
await _service.removeFromLockFolder(ids);
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to remove assets from lock folder', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> trash(ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
|
||||
@@ -431,35 +377,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> stack(String userId, ActionSource source) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
await _service.stack(userId, ids);
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to stack assets', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> unStack(ActionSource source) async {
|
||||
final assets = _getOwnedRemoteAssetsForSource(source);
|
||||
try {
|
||||
await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList());
|
||||
if (source == ActionSource.viewer) {
|
||||
final updatedParent = await _assetService.getRemoteAsset(assets.first.id);
|
||||
if (updatedParent != null) {
|
||||
ref.read(assetViewerProvider.notifier).setAsset(updatedParent);
|
||||
}
|
||||
}
|
||||
|
||||
return ActionResult(count: assets.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to unstack assets', error, stack);
|
||||
return ActionResult(count: assets.length, success: false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> shareAssets(
|
||||
ActionSource source,
|
||||
BuildContext context, {
|
||||
|
||||
@@ -12,25 +12,24 @@ import 'package:immich_mobile/presentation/actions/action.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/cast.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/similar_photos.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/slideshow.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.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';
|
||||
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';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_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/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_lock_folder_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_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_link_action_button.widget.dart';
|
||||
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/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -205,7 +204,7 @@ enum ActionButtonType {
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.slideshow => ActionMenuItemWidget(action: SlideshowAction(scope: scope)),
|
||||
ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItemWidget(
|
||||
action: ArchiveAction(assets: [context.asset], scope: scope),
|
||||
),
|
||||
@@ -220,15 +219,11 @@ enum ActionButtonType {
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.moveToLockFolder => ActionMenuItemWidget(
|
||||
action: LockAction(assets: [context.asset], scope: scope),
|
||||
),
|
||||
ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.removeFromLockFolder => ActionMenuItemWidget(
|
||||
action: LockAction(assets: [context.asset], scope: scope),
|
||||
),
|
||||
ActionButtonType.deleteLocal => DeleteLocalActionButton(
|
||||
source: context.source,
|
||||
@@ -252,21 +247,14 @@ enum ActionButtonType {
|
||||
ActionButtonType.unstack => ActionMenuItemWidget(
|
||||
action: StackAction(assets: [context.asset], scope: scope),
|
||||
),
|
||||
ActionButtonType.openInBrowser => OpenInBrowserActionButton(
|
||||
remoteId: context.asset.remoteId!,
|
||||
origin: context.timelineOrigin,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.openInBrowser => ActionMenuItemWidget(
|
||||
action: OpenInBrowserAction(remoteId: context.asset.remoteId!, origin: context.timelineOrigin, scope: scope),
|
||||
),
|
||||
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
|
||||
assetId: (context.asset as RemoteAsset).id,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.similarPhotos => ActionMenuItemWidget(
|
||||
action: SimilarPhotosAction(assetId: (context.asset as RemoteAsset).id, scope: scope),
|
||||
),
|
||||
ActionButtonType.setProfilePicture => SetProfilePictureActionButton(
|
||||
asset: context.asset,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
ActionButtonType.setProfilePicture => ActionMenuItemWidget(
|
||||
action: SetProfilePictureAction(asset: context.asset, scope: scope),
|
||||
),
|
||||
ActionButtonType.openInfo => BaseActionButton(
|
||||
label: 'info'.tr(),
|
||||
@@ -284,7 +272,7 @@ enum ActionButtonType {
|
||||
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
|
||||
},
|
||||
),
|
||||
ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.cast => ActionMenuItemWidget(action: CastAction(scope: scope)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ extension type const AssetFilter<T extends BaseAsset>(Iterable<T> assets) implem
|
||||
remote().where((asset) => asset.visibility != visibility);
|
||||
AssetFilter<RemoteAsset> archived({bool isArchived = true}) =>
|
||||
remote().where((asset) => asset.isArchived == isArchived);
|
||||
AssetFilter<RemoteAsset> locked({bool isLocked = true}) => remote().where((asset) => asset.isLocked == isLocked);
|
||||
AssetFilter<RemoteAsset> stacked({bool isStacked = true}) => remote().where((asset) => asset.isStacked == isStacked);
|
||||
AssetFilter<RemoteAsset> trashed({bool isTrashed = true}) => remote().where((asset) => asset.isTrashed == isTrashed);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockStoreService extends Mock implements StoreService {}
|
||||
@@ -20,3 +21,5 @@ class MockPartnerService extends Mock implements PartnerService {}
|
||||
class MockAssetService extends Mock implements AssetService {}
|
||||
|
||||
class MockUserService extends Mock implements UserService {}
|
||||
|
||||
class MockGCastService extends Mock implements GCastService {}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/lock.action.dart';
|
||||
import 'package:immich_mobile/utils/option.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 = .timeline}) =>
|
||||
RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility);
|
||||
|
||||
group('LockAction', () {
|
||||
testWidgets('locks the eligible owned assets', (tester) async {
|
||||
final asset = owned();
|
||||
final action = await tester.pumpTestAction(context, (scope) => LockAction(assets: [asset], scope: scope));
|
||||
|
||||
expect(action.icon, Icons.lock_rounded);
|
||||
expect(action.label, StaticTranslations.instance.move_to_locked_folder);
|
||||
|
||||
verify(() => assetService.update([asset.id], visibility: const Some(.locked))).called(1);
|
||||
});
|
||||
|
||||
testWidgets('unlocks the eligible owned assets', (tester) async {
|
||||
final asset = owned(visibility: .locked);
|
||||
final action = await tester.pumpTestAction(context, (scope) => LockAction(assets: [asset], scope: scope));
|
||||
|
||||
expect(action.icon, Icons.lock_open_rounded);
|
||||
expect(action.label, StaticTranslations.instance.remove_from_locked_folder);
|
||||
|
||||
verify(() => assetService.update([asset.id], visibility: const Some(.timeline))).called(1);
|
||||
});
|
||||
|
||||
testWidgets('dispatches on owned state, ignoring assets owned by others', (tester) async {
|
||||
final mine = owned(visibility: .locked);
|
||||
final theirs = RemoteAssetFactory.create();
|
||||
final action = await tester.pumpTestAction(context, (scope) => LockAction(assets: [mine, theirs], scope: scope));
|
||||
expect(action.label, StaticTranslations.instance.remove_from_locked_folder);
|
||||
|
||||
verify(() => assetService.update([mine.id], visibility: const Some(.timeline))).called(1);
|
||||
});
|
||||
|
||||
testWidgets('batches every eligible owned asset into a single call', (tester) async {
|
||||
final first = owned();
|
||||
final second = owned();
|
||||
|
||||
await tester.pumpTestAction(context, (scope) => LockAction(assets: [first, second], scope: scope));
|
||||
|
||||
verify(() => assetService.update([first.id, second.id], visibility: const Some(.locked))).called(1);
|
||||
});
|
||||
|
||||
testWidgets('locks only the owned assets not already locked', (tester) async {
|
||||
final stale = owned();
|
||||
final alreadyLocked = owned(visibility: .locked);
|
||||
|
||||
await tester.pumpTestAction(context, (scope) => LockAction(assets: [stale, alreadyLocked], scope: scope));
|
||||
|
||||
verify(() => assetService.update([stale.id], visibility: const Some(.locked))).called(1);
|
||||
});
|
||||
|
||||
testWidgets('reports the locked count through the toast repository', (tester) async {
|
||||
final toast = context.repository.toast;
|
||||
|
||||
await tester.pumpTestAction(context, (scope) => LockAction(assets: [owned(), owned()], scope: scope));
|
||||
|
||||
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||
expect(message, StaticTranslations.instance.move_to_lock_folder_action_prompt(count: 2));
|
||||
});
|
||||
|
||||
testWidgets('reports the unlocked count through the toast repository', (tester) async {
|
||||
final toast = context.repository.toast;
|
||||
|
||||
await tester.pumpTestAction(
|
||||
context,
|
||||
(scope) => LockAction(
|
||||
assets: [
|
||||
owned(visibility: .locked),
|
||||
owned(visibility: .locked),
|
||||
],
|
||||
scope: scope,
|
||||
),
|
||||
);
|
||||
|
||||
final message = verify(() => toast.success(captureAny())).captured.single as String;
|
||||
expect(message, StaticTranslations.instance.remove_from_lock_folder_action_prompt(count: 2));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -17,9 +17,11 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../domain/service.mock.dart';
|
||||
import '../../test_utils.dart';
|
||||
import '../factories/user_factory.dart';
|
||||
import '../mocks.dart';
|
||||
@@ -45,6 +47,7 @@ class PresentationContext {
|
||||
assetServiceProvider.overrideWithValue(service.asset.service),
|
||||
partnerServiceProvider.overrideWithValue(service.partner.service),
|
||||
toastRepositoryProvider.overrideWithValue(repository.toast),
|
||||
gCastServiceProvider.overrideWithValue(MockGCastService()),
|
||||
];
|
||||
|
||||
static Future<PresentationContext> create() async {
|
||||
|
||||
Reference in New Issue
Block a user