diff --git a/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart index cb2581bc6d..b68ab69b26 100644 --- a/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart @@ -9,8 +9,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; class AdvancedInfoActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const AdvancedInfoActionButton({super.key, required this.source}); + const AdvancedInfoActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -26,6 +28,8 @@ class AdvancedInfoActionButton extends ConsumerWidget { maxWidth: 115.0, iconData: Icons.help_outline_rounded, label: "troubleshoot".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart index 4ba877bcba..7c7e96c1c5 100644 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -35,8 +35,10 @@ Future performArchiveAction(BuildContext context, WidgetRef ref, {required class ArchiveActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const ArchiveActionButton({super.key, required this.source}); + const ArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); Future _onTap(BuildContext context, WidgetRef ref) async { await performArchiveAction(context, ref, source: source); @@ -47,6 +49,8 @@ class ArchiveActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.archive_outlined, label: "to_archive".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart index 8b82e5c839..94ee1b2343 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -18,7 +18,15 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DeleteActionButton extends ConsumerWidget { final ActionSource source; final bool showConfirmation; - const DeleteActionButton({super.key, required this.source, this.showConfirmation = false}); + final bool iconOnly; + final bool menuItem; + const DeleteActionButton({ + super.key, + required this.source, + this.showConfirmation = false, + this.iconOnly = false, + this.menuItem = false, + }); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -74,6 +82,8 @@ class DeleteActionButton extends ConsumerWidget { maxWidth: 110.0, iconData: Icons.delete_sweep_outlined, label: "delete".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart index 5d8ea8671c..2e15de49df 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -14,8 +14,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// - Prompt to delete the asset locally class DeleteLocalActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const DeleteLocalActionButton({super.key, required this.source}); + const DeleteLocalActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -55,6 +57,8 @@ class DeleteLocalActionButton extends ConsumerWidget { maxWidth: 95.0, iconData: Icons.no_cell_outlined, label: "control_bottom_app_bar_delete_from_local".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart index a0191e2407..2dd9a265ed 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart @@ -15,8 +15,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// - Prompt to delete the asset locally class DeletePermanentActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const DeletePermanentActionButton({super.key, required this.source}); + const DeletePermanentActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -51,6 +53,8 @@ class DeletePermanentActionButton extends ConsumerWidget { maxWidth: 110.0, iconData: Icons.delete_forever, label: "delete_permanently".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart index 20d391c4a6..341791c1af 100644 --- a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart @@ -38,8 +38,10 @@ Future performMoveToLockFolderAction(BuildContext context, WidgetRef ref, class MoveToLockFolderActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const MoveToLockFolderActionButton({super.key, required this.source}); + const MoveToLockFolderActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); Future _onTap(BuildContext context, WidgetRef ref) async { await performMoveToLockFolderAction(context, ref, source: source); @@ -51,6 +53,8 @@ class MoveToLockFolderActionButton extends ConsumerWidget { maxWidth: 115.0, iconData: Icons.lock_outline_rounded, label: "move_to_locked_folder".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart index 2ce8b345a1..fd88e94cf7 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart @@ -2,6 +2,8 @@ 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'; @@ -11,8 +13,16 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class RemoveFromAlbumActionButton extends ConsumerWidget { final String albumId; final ActionSource source; + final bool iconOnly; + final bool menuItem; - const RemoveFromAlbumActionButton({super.key, required this.albumId, required this.source}); + const RemoveFromAlbumActionButton({ + super.key, + required this.albumId, + required this.source, + this.iconOnly = false, + this.menuItem = false, + }); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -22,6 +32,10 @@ class RemoveFromAlbumActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); ref.read(multiSelectProvider.notifier).reset(); + if (source == ActionSource.viewer) { + EventStream.shared.emit(const ViewerReloadAssetEvent()); + } + final successMessage = 'remove_from_album_action_prompt'.t( context: context, args: {'count': result.count.toString()}, @@ -42,6 +56,8 @@ class RemoveFromAlbumActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.remove_circle_outline, label: "remove_from_album".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), maxWidth: 100, ); diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart index 028abf5596..17d2a76af7 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart @@ -10,8 +10,15 @@ 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}); + const RemoveFromLockFolderActionButton({ + super.key, + required this.source, + this.iconOnly = false, + this.menuItem = false, + }); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -42,6 +49,8 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget { maxWidth: 100.0, iconData: Icons.lock_open_rounded, label: "remove_from_locked_folder".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 6bcf099487..4f272cb990 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -31,8 +31,10 @@ class _SharePreparingDialog extends StatelessWidget { class ShareActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const ShareActionButton({super.key, required this.source}); + const ShareActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -74,6 +76,8 @@ class ShareActionButton extends ConsumerWidget { return BaseActionButton( iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, label: 'share'.t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart index 4a9f6d9bd6..b8dc69f515 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart @@ -7,8 +7,10 @@ import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; class ShareLinkActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const ShareLinkActionButton({super.key, required this.source}); + const ShareLinkActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -23,6 +25,8 @@ class ShareLinkActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.link_rounded, label: "share_link".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index 4cbc0f0bb8..65ba744ec3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -13,8 +13,10 @@ import 'package:immich_mobile/routing/router.dart'; class SimilarPhotosActionButton extends ConsumerWidget { final String assetId; + final bool iconOnly; + final bool menuItem; - const SimilarPhotosActionButton({super.key, required this.assetId}); + const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -44,6 +46,8 @@ class SimilarPhotosActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.compare, label: "view_similar_photos".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), maxWidth: 100, ); diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart index a78ff2ccd8..5f1e385769 100644 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -15,8 +15,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// which will be permanently deleted after the number of days configure by the admin class TrashActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const TrashActionButton({super.key, required this.source}); + const TrashActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -48,6 +50,8 @@ class TrashActionButton extends ConsumerWidget { maxWidth: 85.0, iconData: Icons.delete_outline_rounded, label: "control_bottom_app_bar_trash_from_immich".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart index 32147a194f..8cf0bcba92 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -37,8 +37,10 @@ Future performUnArchiveAction(BuildContext context, WidgetRef ref, {requir class UnArchiveActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const UnArchiveActionButton({super.key, required this.source}); + const UnArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); Future _onTap(BuildContext context, WidgetRef ref) async { await performUnArchiveAction(context, ref, source: source); @@ -49,6 +51,8 @@ class UnArchiveActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.unarchive_outlined, label: "unarchive".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart index a07803ace5..e7badf129f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -10,8 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class UnStackActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const UnStackActionButton({super.key, required this.source}); + const UnStackActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -38,6 +40,8 @@ class UnStackActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.layers_clear_outlined, label: "unstack".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart index f037d365d8..98ef831f9c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart @@ -10,8 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class UploadActionButton extends ConsumerWidget { final ActionSource source; + final bool iconOnly; + final bool menuItem; - const UploadActionButton({super.key, required this.source}); + const UploadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); void _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { @@ -39,6 +41,8 @@ class UploadActionButton extends ConsumerWidget { return BaseActionButton( iconData: Icons.backup_outlined, label: "upload".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, onPressed: () => _onTap(context, ref), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 67bbc4c83a..537f2fc31d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -42,14 +42,17 @@ class ViewerBottomBar extends ConsumerWidget { final actions = [ const ShareActionButton(source: ActionSource.viewer), - if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - if (asset.type == AssetType.image) const EditImageActionButton(), - if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), - if (isOwner) ...[ - asset.isLocalOnly - ? const DeleteLocalActionButton(source: ActionSource.viewer) - : const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true), + if (!isInLockedView) ...[ + if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), + if (asset.type == AssetType.image) const EditImageActionButton(), + if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), + + if (isOwner) ...[ + asset.isLocalOnly + ? const DeleteLocalActionButton(source: ActionSource.viewer) + : const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true), + ], ], ]; @@ -76,7 +79,7 @@ class ViewerBottomBar extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ if (asset.isVideo) const VideoControls(), - if (!isInLockedView && !isReadonlyModeEnabled) + if (!isReadonlyModeEnabled) Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), ], ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 80840d94b4..f0ba970b98 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.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/exif.model.dart'; -import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -21,14 +20,9 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/action_button.utils.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/timezone.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -48,29 +42,8 @@ class AssetDetailBottomSheet extends ConsumerWidget { return const SizedBox.shrink(); } - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); - final isOwner = asset is RemoteAsset && asset.ownerId == ref.watch(currentUserProvider)?.id; - final isInLockedView = ref.watch(inLockedViewProvider); - final currentAlbum = ref.watch(currentRemoteAlbumProvider); - final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; - final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); - - final buttonContext = ActionButtonContext( - asset: asset, - isOwner: isOwner, - isArchived: isArchived, - isTrashEnabled: isTrashEnable, - isInLockedView: isInLockedView, - isStacked: asset is RemoteAsset && asset.stackId != null, - currentAlbum: currentAlbum, - advancedTroubleshooting: advancedTroubleshooting, - source: ActionSource.viewer, - ); - - final actions = ActionButtonBuilder.build(buttonContext); - return BaseBottomSheet( - actions: actions, + actions: [], slivers: const [_AssetDetailBottomSheet()], controller: controller, initialChildSize: initialChildSize, diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart index ff638ee583..10f3595d01 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/utils/action_button.utils.dart'; @@ -24,16 +30,28 @@ class ViewerKebabMenu extends ConsumerWidget { final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final timelineOrigin = ref.read(timelineServiceProvider).origin; + final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); + final isInLockedView = ref.watch(inLockedViewProvider); + final currentAlbum = ref.watch(currentRemoteAlbumProvider); + final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; + final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); - final kebabContext = ViewerKebabMenuButtonContext( + final actionContext = ActionButtonContext( asset: asset, isOwner: isOwner, + isArchived: isArchived, + isTrashEnabled: isTrashEnable, + isStacked: asset is RemoteAsset && asset.stackId != null, + isInLockedView: isInLockedView, + currentAlbum: currentAlbum, + advancedTroubleshooting: advancedTroubleshooting, + source: ActionSource.viewer, isCasting: isCasting, timelineOrigin: timelineOrigin, originalTheme: originalTheme, ); - final menuChildren = ViewerKebabMenuButtonBuilder.build(kebabContext, context, ref); + final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref); return MenuAnchor( consumeOutsideTap: true, diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 917ddbebca..1a2883bee7 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; 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/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; @@ -40,6 +39,9 @@ class ActionButtonContext { final RemoteAlbum? currentAlbum; final bool advancedTroubleshooting; final ActionSource source; + final bool isCasting; + final TimelineOrigin timelineOrigin; + final ThemeData? originalTheme; const ActionButtonContext({ required this.asset, @@ -51,27 +53,33 @@ class ActionButtonContext { required this.currentAlbum, required this.advancedTroubleshooting, required this.source, + this.isCasting = false, + this.timelineOrigin = TimelineOrigin.main, + this.originalTheme, }); } enum ActionButtonType { - advancedInfo, + openInfo, + likeActivity, share, shareLink, + cast, similarPhotos, + viewInTimeline, + download, + upload, + unstack, archive, unarchive, - download, - trash, - deletePermanent, - delete, moveToLockFolder, removeFromLockFolder, - deleteLocal, - upload, removeFromAlbum, - unstack, - likeActivity; + trash, + deleteLocal, + deletePermanent, + delete, + advancedInfo; bool shouldShow(ActionButtonContext context) { return switch (this) { @@ -138,132 +146,163 @@ enum ActionButtonType { ActionButtonType.similarPhotos => !context.isInLockedView && // context.asset is RemoteAsset, - }; - } - - Widget buildButton(ActionButtonContext context) { - return switch (this) { - ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source), - ActionButtonType.share => ShareActionButton(source: context.source), - ActionButtonType.shareLink => ShareLinkActionButton(source: context.source), - ActionButtonType.archive => ArchiveActionButton(source: context.source), - ActionButtonType.unarchive => UnArchiveActionButton(source: context.source), - ActionButtonType.download => DownloadActionButton(source: context.source), - ActionButtonType.trash => TrashActionButton(source: context.source), - ActionButtonType.deletePermanent => DeletePermanentActionButton(source: context.source), - ActionButtonType.delete => DeleteActionButton(source: context.source), - ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton(source: context.source), - ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton(source: context.source), - ActionButtonType.deleteLocal => DeleteLocalActionButton(source: context.source), - ActionButtonType.upload => UploadActionButton(source: context.source), - ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - ), - ActionButtonType.likeActivity => const LikeActivityActionButton(), - ActionButtonType.unstack => UnStackActionButton(source: context.source), - ActionButtonType.similarPhotos => SimilarPhotosActionButton(assetId: (context.asset as RemoteAsset).id), - }; - } -} - -class ActionButtonBuilder { - static const List _actionTypes = ActionButtonType.values; - - static List build(ActionButtonContext context) { - return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); - } -} - -class ViewerKebabMenuButtonContext { - final BaseAsset asset; - final bool isOwner; - final bool isCasting; - final TimelineOrigin timelineOrigin; - final ThemeData? originalTheme; - - const ViewerKebabMenuButtonContext({ - required this.asset, - required this.isOwner, - required this.isCasting, - required this.timelineOrigin, - this.originalTheme, - }); -} - -enum ViewerKebabMenuButtonType { - openInfo, - viewInTimeline, - cast, - download; - - /// Defines which group each button belongs to. - /// Buttons in the same group will be displayed together, - /// with dividers separating different groups. - int get group => switch (this) { - ViewerKebabMenuButtonType.openInfo => 0, - ViewerKebabMenuButtonType.viewInTimeline => 1, - ViewerKebabMenuButtonType.cast => 1, - ViewerKebabMenuButtonType.download => 1, - }; - - bool shouldShow(ViewerKebabMenuButtonContext context) { - return switch (this) { - ViewerKebabMenuButtonType.openInfo => true, - ViewerKebabMenuButtonType.viewInTimeline => + ActionButtonType.openInfo => true, + ActionButtonType.viewInTimeline => context.timelineOrigin != TimelineOrigin.main && context.timelineOrigin != TimelineOrigin.deepLink && context.timelineOrigin != TimelineOrigin.trash && + context.timelineOrigin != TimelineOrigin.lockedFolder && context.timelineOrigin != TimelineOrigin.archive && context.timelineOrigin != TimelineOrigin.localAlbum && context.isOwner, - ViewerKebabMenuButtonType.cast => context.isCasting || context.asset.hasRemote, - ViewerKebabMenuButtonType.download => context.asset.isRemoteOnly, + ActionButtonType.cast => context.isCasting || context.asset.hasRemote, }; } - ConsumerWidget buildButton(ViewerKebabMenuButtonContext context, BuildContext buildContext) { + ConsumerWidget buildButton( + ActionButtonContext context, [ + BuildContext? buildContext, + bool iconOnly = false, + bool menuItem = false, + ]) { return switch (this) { - ViewerKebabMenuButtonType.openInfo => BaseActionButton( + ActionButtonType.advancedInfo => AdvancedInfoActionButton( + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.shareLink => ShareLinkActionButton( + source: context.source, + 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.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.deletePermanent => DeletePermanentActionButton( + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton( + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton( + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.deleteLocal => DeleteLocalActionButton( + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( + albumId: context.currentAlbum!.id, + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.similarPhotos => SimilarPhotosActionButton( + assetId: (context.asset as RemoteAsset).id, + iconOnly: iconOnly, + menuItem: menuItem, + ), + ActionButtonType.openInfo => BaseActionButton( label: 'info'.tr(), iconData: Icons.info_outline, iconColor: context.originalTheme?.iconTheme.color, menuItem: true, onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()), ), - - ViewerKebabMenuButtonType.viewInTimeline => BaseActionButton( - label: 'view_in_timeline'.t(context: buildContext), + ActionButtonType.viewInTimeline => BaseActionButton( + label: 'view_in_timeline'.tr(), iconData: Icons.image_search, iconColor: context.originalTheme?.iconTheme.color, - menuItem: true, - onPressed: () async { - await buildContext.maybePop(); - await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()])); - EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt)); - }, + iconOnly: iconOnly, + menuItem: menuItem, + onPressed: buildContext == null + ? null + : () async { + await buildContext.maybePop(); + await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()])); + EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt)); + }, ), - ViewerKebabMenuButtonType.cast => const CastActionButton(menuItem: true), - ViewerKebabMenuButtonType.download => const DownloadActionButton(source: ActionSource.viewer, menuItem: true), + ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem), }; } + + /// Defines which group each button belongs to for kebab menu. + /// Buttons in the same group will be displayed together, + /// with dividers separating different groups. + int get kebabMenuGroup => switch (this) { + // 0: info + ActionButtonType.openInfo => 0, + // 10: move,remove, and delete + ActionButtonType.trash => 10, + ActionButtonType.deletePermanent => 10, + ActionButtonType.removeFromLockFolder => 10, + ActionButtonType.removeFromAlbum => 10, + ActionButtonType.unstack => 10, + ActionButtonType.archive => 10, + ActionButtonType.unarchive => 10, + ActionButtonType.moveToLockFolder => 10, + ActionButtonType.deleteLocal => 10, + ActionButtonType.delete => 10, + // 90: advancedInfo + ActionButtonType.advancedInfo => 90, + // 1: others + _ => 1, + }; } -class ViewerKebabMenuButtonBuilder { - static List build(ViewerKebabMenuButtonContext context, BuildContext buildContext, WidgetRef ref) { - final visibleButtons = ViewerKebabMenuButtonType.values.where((type) => type.shouldShow(context)).toList(); +class ActionButtonBuilder { + static const List _actionTypes = ActionButtonType.values; + static const List defaultViewerKebabMenuOrder = _actionTypes; + static const Set defaultViewerBottomBarButtons = { + ActionButtonType.share, + ActionButtonType.moveToLockFolder, + ActionButtonType.upload, + ActionButtonType.delete, + ActionButtonType.archive, + ActionButtonType.unarchive, + }; - if (visibleButtons.isEmpty) return []; + static List build(ActionButtonContext context) { + return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); + } + + static List buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + final visibleButtons = defaultViewerKebabMenuOrder + .where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context)) + .toList(); + + if (visibleButtons.isEmpty) { + return []; + } final List result = []; int? lastGroup; for (final type in visibleButtons) { - if (lastGroup != null && type.group != lastGroup) { + if (lastGroup != null && type.kebabMenuGroup != lastGroup) { result.add(const Divider(height: 1)); } - result.add(type.buildButton(context, buildContext).build(buildContext, ref)); - lastGroup = type.group; + result.add(type.buildButton(context, buildContext, false, true).build(buildContext, ref)); + lastGroup = type.kebabMenuGroup; } return result;