diff --git a/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart new file mode 100644 index 0000000000..ac93b3f3a7 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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'; + +class OpenActivityActionButton extends ConsumerWidget { + const OpenActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false}); + + final bool iconOnly; + final bool menuItem; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.chat_outlined, + label: "activity".t(context: context), + onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)), + iconOnly: iconOnly, + menuItem: menuItem, + ); + } +} 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 537f2fc31d..f9f3c3478d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -2,18 +2,18 @@ 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/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/edit_image_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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/readonly_mode.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'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; class ViewerBottomBar extends ConsumerWidget { @@ -33,6 +33,11 @@ class ViewerBottomBar extends ConsumerWidget { int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final isInLockedView = ref.watch(inLockedViewProvider); + final album = ref.watch(currentRemoteAlbumProvider); + final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; + final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); + final timelineOrigin = ref.read(timelineServiceProvider).origin; + final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); if (!showControls) { opacity = 0; @@ -40,21 +45,21 @@ class ViewerBottomBar extends ConsumerWidget { final originalTheme = context.themeData; - final actions = [ - const ShareActionButton(source: ActionSource.viewer), + final buttonContext = ActionButtonContext( + asset: asset, + isOwner: isOwner, + isArchived: isArchived, + isTrashEnabled: isTrashEnable, + isStacked: asset is RemoteAsset && asset.stackId != null, + isInLockedView: isInLockedView, + currentAlbum: album, + advancedTroubleshooting: advancedTroubleshooting, + source: ActionSource.viewer, + timelineOrigin: timelineOrigin, + originalTheme: originalTheme, + ); - 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), - ], - ], - ]; + final actions = ActionButtonBuilder.buildViewerBottomBar(buttonContext, context, ref); return IgnorePointer( ignoring: opacity < 255, diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index f5e79c1ec4..610617acfc 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -3,8 +3,6 @@ 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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 9383fd16d0..30f06793e5 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -17,6 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_a 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/open_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/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; @@ -27,6 +28,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; class ActionButtonContext { @@ -70,6 +73,8 @@ enum ActionButtonType { viewInTimeline, download, upload, + editImage, + addTo, unstack, archive, unarchive, @@ -85,7 +90,9 @@ enum ActionButtonType { bool shouldShow(ActionButtonContext context) { return switch (this) { ActionButtonType.advancedInfo => context.advancedTroubleshooting, - ActionButtonType.share => true, + ActionButtonType.share => + !context.isInLockedView && // + context.asset.hasRemote, ActionButtonType.shareLink => !context.isInLockedView && // context.asset.hasRemote, @@ -131,6 +138,12 @@ enum ActionButtonType { ActionButtonType.upload => !context.isInLockedView && // context.asset.storage == AssetState.local, + ActionButtonType.editImage => + !context.isInLockedView && // + context.asset.type == AssetType.image, + ActionButtonType.addTo => + !context.isInLockedView && // + context.asset.hasRemote, ActionButtonType.removeFromAlbum => context.isOwner && // !context.isInLockedView && // @@ -165,7 +178,7 @@ enum ActionButtonType { }; } - ConsumerWidget buildButton( + Widget buildButton( ActionButtonContext context, [ BuildContext? buildContext, bool iconOnly = false, @@ -213,6 +226,8 @@ enum ActionButtonType { menuItem: menuItem, ), ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.editImage => const EditImageActionButton(), + ActionButtonType.addTo => AddActionButton(originalTheme: context.originalTheme), ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( albumId: context.currentAlbum!.id, source: context.source, @@ -233,13 +248,7 @@ enum ActionButtonType { menuItem: true, onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()), ), - ActionButtonType.openActivity => BaseActionButton( - label: 'activity'.tr(), - iconData: Icons.chat_outlined, - iconColor: context.originalTheme?.iconTheme.color, - menuItem: true, - onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)), - ), + ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.viewInTimeline => BaseActionButton( label: 'view_in_timeline'.tr(), iconData: Icons.image_search, @@ -285,22 +294,40 @@ enum ActionButtonType { class ActionButtonBuilder { static const List _actionTypes = ActionButtonType.values; static const List defaultViewerKebabMenuOrder = _actionTypes; - static const Set defaultViewerBottomBarButtons = { + static const List _defaultViewerBottomBarOrder = [ ActionButtonType.share, - ActionButtonType.moveToLockFolder, ActionButtonType.upload, + ActionButtonType.editImage, + ActionButtonType.openActivity, + ActionButtonType.likeActivity, + ActionButtonType.addTo, + ActionButtonType.deleteLocal, ActionButtonType.delete, - ActionButtonType.archive, - ActionButtonType.unarchive, - }; + ActionButtonType.removeFromLockFolder, + ActionButtonType.deletePermanent, + ]; 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) { + // Get visible bottom bar buttons for exclusion + final visibleBottomBarButtons = _defaultViewerBottomBarOrder + .where((type) => type.shouldShow(context)) + .take(4) + .toSet(); + + // Always exclude addTo from kebab menu + final excludedTypes = {...visibleBottomBarButtons, ActionButtonType.addTo}; + + // If addTo is visible in bottom bar, also exclude moveToLockFolder, archive, and unarchive from kebab menu + if (visibleBottomBarButtons.contains(ActionButtonType.addTo)) { + excludedTypes.addAll([ActionButtonType.moveToLockFolder, ActionButtonType.archive, ActionButtonType.unarchive]); + } + final visibleButtons = defaultViewerKebabMenuOrder - .where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context)) + .where((type) => !excludedTypes.contains(type) && type.shouldShow(context)) .toList(); if (visibleButtons.isEmpty) { @@ -314,10 +341,21 @@ class ActionButtonBuilder { if (lastGroup != null && type.kebabMenuGroup != lastGroup) { result.add(const Divider(height: 1)); } - result.add(type.buildButton(context, buildContext, false, true).build(buildContext, ref)); + final widget = type.buildButton(context, buildContext, false, true); + result.add(widget is ConsumerWidget ? widget.build(buildContext, ref) : widget); lastGroup = type.kebabMenuGroup; } return result; } + + static List buildViewerBottomBar(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + // Take only the first 4 visible buttons from the order + final visibleButtons = _defaultViewerBottomBarOrder.where((type) => type.shouldShow(context)).take(4).toList(); + + return visibleButtons.map((type) { + final widget = type.buildButton(context, buildContext, false, false); + return widget is ConsumerWidget ? widget.build(buildContext, ref) : widget; + }).toList(); + } }