From 7d4de5f2e2fd5635a0a16aece984c8d92ea6e6be Mon Sep 17 00:00:00 2001 From: idubnori Date: Tue, 16 Dec 2025 16:58:06 +0900 Subject: [PATCH 01/11] feat(mobile): move activity button to action button menu --- .../widgets/asset_viewer/top_app_bar.widget.dart | 7 ------- mobile/lib/utils/action_button.utils.dart | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) 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 193cf60220..f5e79c1ec4 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 @@ -51,13 +51,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final actions = [ if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true), - if (album != null && album.isActivityEnabled && album.isShared) - IconButton( - icon: const Icon(Icons.chat_outlined), - onPressed: () { - EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)); - }, - ), if (asset.hasRemote && isOwner && !asset.isFavorite) const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true), diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 1a2883bee7..9383fd16d0 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -61,6 +61,7 @@ class ActionButtonContext { enum ActionButtonType { openInfo, + openActivity, likeActivity, share, shareLink, @@ -138,6 +139,11 @@ enum ActionButtonType { context.isOwner && // !context.isInLockedView && // context.isStacked, + ActionButtonType.openActivity => + !context.isInLockedView && + context.currentAlbum != null && + context.currentAlbum!.isActivityEnabled && + context.currentAlbum!.isShared, ActionButtonType.likeActivity => !context.isInLockedView && context.currentAlbum != null && @@ -227,6 +233,13 @@ 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.viewInTimeline => BaseActionButton( label: 'view_in_timeline'.tr(), iconData: Icons.image_search, @@ -249,7 +262,7 @@ enum ActionButtonType { /// Buttons in the same group will be displayed together, /// with dividers separating different groups. int get kebabMenuGroup => switch (this) { - // 0: info + // 0: info and activity ActionButtonType.openInfo => 0, // 10: move,remove, and delete ActionButtonType.trash => 10, From 76fd68957c7a91eeccc3b29a966b0498976a9e2f Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 11:43:42 +0900 Subject: [PATCH 02/11] feat(mobile): improve action button order --- .../open_activity_action_button.widget.dart | 24 +++++++ .../asset_viewer/bottom_bar.widget.dart | 45 ++++++------ .../asset_viewer/top_app_bar.widget.dart | 2 - mobile/lib/utils/action_button.utils.dart | 70 ++++++++++++++----- 4 files changed, 103 insertions(+), 38 deletions(-) create mode 100644 mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart 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(); + } } From c38ecab1a640597cbff018c37b15292b8438faf7 Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 13:31:53 +0900 Subject: [PATCH 03/11] refactor: action button visibility logic and kebab menu handling --- mobile/lib/utils/action_button.utils.dart | 46 +++++++++++++---------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 30f06793e5..0af8372a8b 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -90,9 +90,7 @@ enum ActionButtonType { bool shouldShow(ActionButtonContext context) { return switch (this) { ActionButtonType.advancedInfo => context.advancedTroubleshooting, - ActionButtonType.share => - !context.isInLockedView && // - context.asset.hasRemote, + ActionButtonType.share => true, ActionButtonType.shareLink => !context.isInLockedView && // context.asset.hasRemote, @@ -311,33 +309,44 @@ class ActionButtonBuilder { 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 + static List getViewerKebabMenuTypes(ActionButtonContext context) { + final visibleBottomBarButtons = getViewerBottomBarTypes(context); 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 + return defaultViewerKebabMenuOrder .where((type) => !excludedTypes.contains(type) && type.shouldShow(context)) .toList(); + } - if (visibleButtons.isEmpty) { + static List getViewerBottomBarTypes(ActionButtonContext context) { + return _defaultViewerBottomBarOrder.where((type) => type.shouldShow(context)).take(4).toList(); + } + + static List buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + final visibleButtons = getViewerKebabMenuTypes(context); + return visibleButtons.toKebabMenuWidgets(context, buildContext, ref); + } + + static List buildViewerBottomBar(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + final visibleButtons = getViewerBottomBarTypes(context); + return visibleButtons.toBottomBarWidgets(context, buildContext, ref); + } +} + +extension ActionButtonTypeListExtension on List { + List toKebabMenuWidgets(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + if (isEmpty) { return []; } final List result = []; int? lastGroup; - for (final type in visibleButtons) { + for (final type in this) { if (lastGroup != null && type.kebabMenuGroup != lastGroup) { result.add(const Divider(height: 1)); } @@ -349,11 +358,8 @@ class ActionButtonBuilder { 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) { + List toBottomBarWidgets(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + return map((type) { final widget = type.buildButton(context, buildContext, false, false); return widget is ConsumerWidget ? widget.build(buildContext, ref) : widget; }).toList(); From 939c222728bc14d46665b60fac01abf01a788373 Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 14:27:38 +0900 Subject: [PATCH 04/11] feat(mobile): add ButtonPosition enum and update action button context --- mobile/lib/constants/enums.dart | 2 + .../asset_viewer/bottom_bar.widget.dart | 1 + mobile/lib/utils/action_button.utils.dart | 7 +- .../test/utils/action_button_utils_test.dart | 98 +++++++++++++++++++ 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 91ca50a2c0..674b988aa2 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -7,3 +7,5 @@ enum AssetVisibilityEnum { timeline, hidden, archive, locked } enum SortUserBy { id } enum ActionSource { timeline, viewer } + +enum ButtonPosition { bottomBar, kebabMenu, other } 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 f9f3c3478d..fb9c70fa1e 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -57,6 +57,7 @@ class ViewerBottomBar extends ConsumerWidget { source: ActionSource.viewer, timelineOrigin: timelineOrigin, originalTheme: originalTheme, + buttonPosition: ButtonPosition.bottomBar, ); final actions = ActionButtonBuilder.buildViewerBottomBar(buttonContext, context, ref); diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 0af8372a8b..547591329c 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -45,6 +45,7 @@ class ActionButtonContext { final bool isCasting; final TimelineOrigin timelineOrigin; final ThemeData? originalTheme; + final ButtonPosition buttonPosition; const ActionButtonContext({ required this.asset, @@ -59,6 +60,7 @@ class ActionButtonContext { this.isCasting = false, this.timelineOrigin = TimelineOrigin.main, this.originalTheme, + this.buttonPosition = ButtonPosition.other, }); } @@ -138,7 +140,8 @@ enum ActionButtonType { context.asset.storage == AssetState.local, ActionButtonType.editImage => !context.isInLockedView && // - context.asset.type == AssetType.image, + context.asset.type == AssetType.image && + !(context.buttonPosition == ButtonPosition.bottomBar && context.currentAlbum?.isShared == true), ActionButtonType.addTo => !context.isInLockedView && // context.asset.hasRemote, @@ -296,9 +299,9 @@ class ActionButtonBuilder { ActionButtonType.share, ActionButtonType.upload, ActionButtonType.editImage, + ActionButtonType.addTo, ActionButtonType.openActivity, ActionButtonType.likeActivity, - ActionButtonType.addTo, ActionButtonType.deleteLocal, ActionButtonType.delete, ActionButtonType.removeFromLockFolder, diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index d93d59d3c7..0cfda00c69 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -962,4 +962,102 @@ void main() { expect(nonArchivedWidgets, isNotEmpty); }); }); + + group('ActionButtonBuilder.getViewerBottomBarTypes', () { + test('should return correct button types for shared album with activity', () { + final remoteAsset = createRemoteAsset(); + final album = createRemoteAlbum(isActivityEnabled: true, isShared: true); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.viewer, + buttonPosition: ButtonPosition.bottomBar, + ); + + final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + + expect(types.length, 4); + expect(types[0], ActionButtonType.share); + expect(types[1], ActionButtonType.addTo); + expect(types[2], ActionButtonType.openActivity); + expect(types[3], ActionButtonType.likeActivity); + }); + + test('should return correct button types for local only asset', () { + final localAsset = createLocalAsset(); + final context = ActionButtonContext( + asset: localAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.viewer, + buttonPosition: ButtonPosition.bottomBar, + ); + + final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + + expect(types.length, 4); + expect(types[0], ActionButtonType.share); + expect(types[1], ActionButtonType.upload); + expect(types[2], ActionButtonType.editImage); + expect(types[3], ActionButtonType.deleteLocal); + }); + + test('should return correct button types for locked view', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: false, + isInLockedView: true, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.viewer, + buttonPosition: ButtonPosition.bottomBar, + ); + + final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + + expect(types.length, 3); + expect(types[0], ActionButtonType.share); + expect(types[1], ActionButtonType.removeFromLockFolder); + expect(types[2], ActionButtonType.deletePermanent); + }); + + test('should return correct button types for remote only asset', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.viewer, + buttonPosition: ButtonPosition.bottomBar, + ); + + final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + + expect(types.length, 4); + expect(types[0], ActionButtonType.share); + expect(types[1], ActionButtonType.editImage); + expect(types[2], ActionButtonType.addTo); + expect(types[3], ActionButtonType.delete); + }); + }); } From e49239e7e98aabec795a225c6f139e0dbc281cca Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 14:40:15 +0900 Subject: [PATCH 05/11] refactor: clean up --- mobile/lib/utils/action_button.utils.dart | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 547591329c..618d218132 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -69,14 +69,13 @@ enum ActionButtonType { openActivity, likeActivity, share, + editImage, shareLink, cast, similarPhotos, viewInTimeline, download, upload, - editImage, - addTo, unstack, archive, unarchive, @@ -87,6 +86,7 @@ enum ActionButtonType { deleteLocal, deletePermanent, delete, + addTo, advancedInfo; bool shouldShow(ActionButtonContext context) { @@ -138,13 +138,6 @@ enum ActionButtonType { ActionButtonType.upload => !context.isInLockedView && // context.asset.storage == AssetState.local, - ActionButtonType.editImage => - !context.isInLockedView && // - context.asset.type == AssetType.image && - !(context.buttonPosition == ButtonPosition.bottomBar && context.currentAlbum?.isShared == true), - ActionButtonType.addTo => - !context.isInLockedView && // - context.asset.hasRemote, ActionButtonType.removeFromAlbum => context.isOwner && // !context.isInLockedView && // @@ -153,11 +146,6 @@ enum ActionButtonType { context.isOwner && // !context.isInLockedView && // context.isStacked, - ActionButtonType.openActivity => - !context.isInLockedView && - context.currentAlbum != null && - context.currentAlbum!.isActivityEnabled && - context.currentAlbum!.isShared, ActionButtonType.likeActivity => !context.isInLockedView && context.currentAlbum != null && @@ -176,6 +164,18 @@ enum ActionButtonType { context.timelineOrigin != TimelineOrigin.localAlbum && context.isOwner, ActionButtonType.cast => context.isCasting || context.asset.hasRemote, + ActionButtonType.editImage => + !context.isInLockedView && // + context.asset.type == AssetType.image && + !(context.buttonPosition == ButtonPosition.bottomBar && context.currentAlbum?.isShared == true), + ActionButtonType.addTo => + !context.isInLockedView && // + context.asset.hasRemote, + ActionButtonType.openActivity => + !context.isInLockedView && + context.currentAlbum != null && + context.currentAlbum!.isActivityEnabled && + context.currentAlbum!.isShared, }; } @@ -227,8 +227,6 @@ 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, @@ -249,7 +247,6 @@ enum ActionButtonType { menuItem: true, onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()), ), - ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.viewInTimeline => BaseActionButton( label: 'view_in_timeline'.tr(), iconData: Icons.image_search, @@ -265,6 +262,9 @@ enum ActionButtonType { }, ), ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.editImage => const EditImageActionButton(), + ActionButtonType.addTo => AddActionButton(originalTheme: context.originalTheme), + ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), }; } @@ -272,7 +272,7 @@ enum ActionButtonType { /// Buttons in the same group will be displayed together, /// with dividers separating different groups. int get kebabMenuGroup => switch (this) { - // 0: info and activity + // 0: info ActionButtonType.openInfo => 0, // 10: move,remove, and delete ActionButtonType.trash => 10, From 6b4cc4e65e9be7bab8c883a77c8c4019ab9ed551 Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 14:56:59 +0900 Subject: [PATCH 06/11] fix: adjust action button layout for better alignment --- .../widgets/asset_viewer/bottom_bar.widget.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 fb9c70fa1e..6f1c171db1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -86,7 +86,11 @@ class ViewerBottomBar extends ConsumerWidget { children: [ if (asset.isVideo) const VideoControls(), if (!isReadonlyModeEnabled) - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: actions.map((action) => Expanded(child: action)).toList(), + ), ], ), ), From a9bee498c4e3466a550a8e28375b8fa69467089f Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 15:19:58 +0900 Subject: [PATCH 07/11] fix: add copyWith method to ActionButtonContext for improved context management --- mobile/lib/utils/action_button.utils.dart | 35 ++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 618d218132..f70be190ef 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -62,6 +62,38 @@ class ActionButtonContext { this.originalTheme, this.buttonPosition = ButtonPosition.other, }); + + ActionButtonContext copyWith({ + BaseAsset? asset, + bool? isOwner, + bool? isArchived, + bool? isTrashEnabled, + bool? isStacked, + bool? isInLockedView, + RemoteAlbum? currentAlbum, + bool? advancedTroubleshooting, + ActionSource? source, + bool? isCasting, + TimelineOrigin? timelineOrigin, + ThemeData? originalTheme, + ButtonPosition? buttonPosition, + }) { + return ActionButtonContext( + asset: asset ?? this.asset, + isOwner: isOwner ?? this.isOwner, + isArchived: isArchived ?? this.isArchived, + isTrashEnabled: isTrashEnabled ?? this.isTrashEnabled, + isStacked: isStacked ?? this.isStacked, + isInLockedView: isInLockedView ?? this.isInLockedView, + currentAlbum: currentAlbum ?? this.currentAlbum, + advancedTroubleshooting: advancedTroubleshooting ?? this.advancedTroubleshooting, + source: source ?? this.source, + isCasting: isCasting ?? this.isCasting, + timelineOrigin: timelineOrigin ?? this.timelineOrigin, + originalTheme: originalTheme ?? this.originalTheme, + buttonPosition: buttonPosition ?? this.buttonPosition, + ); + } } enum ActionButtonType { @@ -326,7 +358,8 @@ class ActionButtonBuilder { } static List getViewerBottomBarTypes(ActionButtonContext context) { - return _defaultViewerBottomBarOrder.where((type) => type.shouldShow(context)).take(4).toList(); + final bottomBarContext = context.copyWith(buttonPosition: ButtonPosition.bottomBar); + return _defaultViewerBottomBarOrder.where((type) => type.shouldShow(bottomBarContext)).take(4).toList(); } static List buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { From 75734b45a0280d0e907e095dfb92f002b3194e33 Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 15:34:56 +0900 Subject: [PATCH 08/11] fix: pass iconOnly and menuItem parameters to EditImageActionButton for enhanced customization --- .../action_buttons/edit_image_action_button.widget.dart | 7 ++++++- mobile/lib/utils/action_button.utils.dart | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index 4c7b6ffbdc..c78ea15021 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -8,7 +8,10 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse import 'package:immich_mobile/routing/router.dart'; class EditImageActionButton extends ConsumerWidget { - const EditImageActionButton({super.key}); + final bool iconOnly; + final bool menuItem; + + const EditImageActionButton({super.key, this.iconOnly = false, this.menuItem = false}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -27,6 +30,8 @@ class EditImageActionButton extends ConsumerWidget { iconData: Icons.tune, label: "edit".t(context: context), onPressed: onPress, + iconOnly: iconOnly, + menuItem: menuItem, ); } } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index f70be190ef..48a52dc0e5 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -294,7 +294,7 @@ enum ActionButtonType { }, ), ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.editImage => const EditImageActionButton(), + ActionButtonType.editImage => EditImageActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.addTo => AddActionButton(originalTheme: context.originalTheme), ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), }; From 8ceb68e240a4b07ee9edcaa1d83bf3c035ce7baa Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 15:45:48 +0900 Subject: [PATCH 09/11] test: verify kebab menu does not contain bottom bar buttons in action button types --- .../test/utils/action_button_utils_test.dart | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index 0cfda00c69..1edd33f36a 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -987,6 +987,12 @@ void main() { expect(types[1], ActionButtonType.addTo); expect(types[2], ActionButtonType.openActivity); expect(types[3], ActionButtonType.likeActivity); + + // Verify kebab menu does not contain bottom bar buttons + final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context); + for (final type in types) { + expect(kebabTypes.contains(type), isFalse); + } }); test('should return correct button types for local only asset', () { @@ -1011,6 +1017,14 @@ void main() { expect(types[1], ActionButtonType.upload); expect(types[2], ActionButtonType.editImage); expect(types[3], ActionButtonType.deleteLocal); + + // Verify kebab menu does not contain bottom bar buttons + final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( + context.copyWith(buttonPosition: ButtonPosition.kebabMenu), + ); + for (final type in types) { + expect(kebabTypes.contains(type), isFalse); + } }); test('should return correct button types for locked view', () { @@ -1034,6 +1048,14 @@ void main() { expect(types[0], ActionButtonType.share); expect(types[1], ActionButtonType.removeFromLockFolder); expect(types[2], ActionButtonType.deletePermanent); + + // Verify kebab menu does not contain bottom bar buttons + final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( + context.copyWith(buttonPosition: ButtonPosition.kebabMenu), + ); + for (final type in types) { + expect(kebabTypes.contains(type), isFalse); + } }); test('should return correct button types for remote only asset', () { @@ -1058,6 +1080,14 @@ void main() { expect(types[1], ActionButtonType.editImage); expect(types[2], ActionButtonType.addTo); expect(types[3], ActionButtonType.delete); + + // Verify kebab menu does not contain bottom bar buttons + final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( + context.copyWith(buttonPosition: ButtonPosition.kebabMenu), + ); + for (final type in types) { + expect(kebabTypes.contains(type), isFalse); + } }); }); } From de36f9a215845a0e469e6c38193f21f8dd60e8ea Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 16:32:52 +0900 Subject: [PATCH 10/11] refactor: update bottom bar button type assertions for consistency and clarity --- .../test/utils/action_button_utils_test.dart | 83 +++++++++---------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index 1edd33f36a..1bb6034aa3 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -980,19 +981,18 @@ void main() { buttonPosition: ButtonPosition.bottomBar, ); - final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + const expectedTypes = [ + ActionButtonType.share, + ActionButtonType.addTo, + ActionButtonType.openActivity, + ActionButtonType.likeActivity, + ]; - expect(types.length, 4); - expect(types[0], ActionButtonType.share); - expect(types[1], ActionButtonType.addTo); - expect(types[2], ActionButtonType.openActivity); - expect(types[3], ActionButtonType.likeActivity); - - // Verify kebab menu does not contain bottom bar buttons + final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes(context); - for (final type in types) { - expect(kebabTypes.contains(type), isFalse); - } + + expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue); + expect(bottomBarTypes.any(kebabTypes.contains), isFalse); }); test('should return correct button types for local only asset', () { @@ -1010,21 +1010,20 @@ void main() { buttonPosition: ButtonPosition.bottomBar, ); - final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + const expectedTypes = [ + ActionButtonType.share, + ActionButtonType.upload, + ActionButtonType.editImage, + ActionButtonType.deleteLocal, + ]; - expect(types.length, 4); - expect(types[0], ActionButtonType.share); - expect(types[1], ActionButtonType.upload); - expect(types[2], ActionButtonType.editImage); - expect(types[3], ActionButtonType.deleteLocal); - - // Verify kebab menu does not contain bottom bar buttons + final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( context.copyWith(buttonPosition: ButtonPosition.kebabMenu), ); - for (final type in types) { - expect(kebabTypes.contains(type), isFalse); - } + + expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue); + expect(bottomBarTypes.any(kebabTypes.contains), isFalse); }); test('should return correct button types for locked view', () { @@ -1042,20 +1041,19 @@ void main() { buttonPosition: ButtonPosition.bottomBar, ); - final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + const expectedTypes = [ + ActionButtonType.share, + ActionButtonType.removeFromLockFolder, + ActionButtonType.deletePermanent, + ]; - expect(types.length, 3); - expect(types[0], ActionButtonType.share); - expect(types[1], ActionButtonType.removeFromLockFolder); - expect(types[2], ActionButtonType.deletePermanent); - - // Verify kebab menu does not contain bottom bar buttons + final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( context.copyWith(buttonPosition: ButtonPosition.kebabMenu), ); - for (final type in types) { - expect(kebabTypes.contains(type), isFalse); - } + + expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue); + expect(bottomBarTypes.any(kebabTypes.contains), isFalse); }); test('should return correct button types for remote only asset', () { @@ -1073,21 +1071,20 @@ void main() { buttonPosition: ButtonPosition.bottomBar, ); - final types = ActionButtonBuilder.getViewerBottomBarTypes(context); + const expectedTypes = [ + ActionButtonType.share, + ActionButtonType.editImage, + ActionButtonType.addTo, + ActionButtonType.delete, + ]; - expect(types.length, 4); - expect(types[0], ActionButtonType.share); - expect(types[1], ActionButtonType.editImage); - expect(types[2], ActionButtonType.addTo); - expect(types[3], ActionButtonType.delete); - - // Verify kebab menu does not contain bottom bar buttons + final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( context.copyWith(buttonPosition: ButtonPosition.kebabMenu), ); - for (final type in types) { - expect(kebabTypes.contains(type), isFalse); - } + + expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue); + expect(bottomBarTypes.any(kebabTypes.contains), isFalse); }); }); } From e966ee554461cffa7306efc553a354ba43d047b9 Mon Sep 17 00:00:00 2001 From: idubnori Date: Wed, 17 Dec 2025 23:37:36 +0900 Subject: [PATCH 11/11] refactor: replace copyWith method with withButtonPosition for ActionButtonContext --- mobile/lib/utils/action_button.utils.dart | 44 +++++++------------ .../test/utils/action_button_utils_test.dart | 6 +-- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 48a52dc0e5..535fe77b9a 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -63,35 +63,21 @@ class ActionButtonContext { this.buttonPosition = ButtonPosition.other, }); - ActionButtonContext copyWith({ - BaseAsset? asset, - bool? isOwner, - bool? isArchived, - bool? isTrashEnabled, - bool? isStacked, - bool? isInLockedView, - RemoteAlbum? currentAlbum, - bool? advancedTroubleshooting, - ActionSource? source, - bool? isCasting, - TimelineOrigin? timelineOrigin, - ThemeData? originalTheme, - ButtonPosition? buttonPosition, - }) { + ActionButtonContext withButtonPosition(ButtonPosition position) { return ActionButtonContext( - asset: asset ?? this.asset, - isOwner: isOwner ?? this.isOwner, - isArchived: isArchived ?? this.isArchived, - isTrashEnabled: isTrashEnabled ?? this.isTrashEnabled, - isStacked: isStacked ?? this.isStacked, - isInLockedView: isInLockedView ?? this.isInLockedView, - currentAlbum: currentAlbum ?? this.currentAlbum, - advancedTroubleshooting: advancedTroubleshooting ?? this.advancedTroubleshooting, - source: source ?? this.source, - isCasting: isCasting ?? this.isCasting, - timelineOrigin: timelineOrigin ?? this.timelineOrigin, - originalTheme: originalTheme ?? this.originalTheme, - buttonPosition: buttonPosition ?? this.buttonPosition, + asset: asset, + isOwner: isOwner, + isArchived: isArchived, + isTrashEnabled: isTrashEnabled, + isStacked: isStacked, + isInLockedView: isInLockedView, + currentAlbum: currentAlbum, + advancedTroubleshooting: advancedTroubleshooting, + source: source, + isCasting: isCasting, + timelineOrigin: timelineOrigin, + originalTheme: originalTheme, + buttonPosition: position, ); } } @@ -358,7 +344,7 @@ class ActionButtonBuilder { } static List getViewerBottomBarTypes(ActionButtonContext context) { - final bottomBarContext = context.copyWith(buttonPosition: ButtonPosition.bottomBar); + final bottomBarContext = context.withButtonPosition(ButtonPosition.bottomBar); return _defaultViewerBottomBarOrder.where((type) => type.shouldShow(bottomBarContext)).take(4).toList(); } diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index 1bb6034aa3..e57b75c552 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -1019,7 +1019,7 @@ void main() { final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( - context.copyWith(buttonPosition: ButtonPosition.kebabMenu), + context.withButtonPosition(ButtonPosition.kebabMenu), ); expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue); @@ -1049,7 +1049,7 @@ void main() { final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( - context.copyWith(buttonPosition: ButtonPosition.kebabMenu), + context.withButtonPosition(ButtonPosition.kebabMenu), ); expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue); @@ -1080,7 +1080,7 @@ void main() { final bottomBarTypes = ActionButtonBuilder.getViewerBottomBarTypes(context); final kebabTypes = ActionButtonBuilder.getViewerKebabMenuTypes( - context.copyWith(buttonPosition: ButtonPosition.kebabMenu), + context.withButtonPosition(ButtonPosition.kebabMenu), ); expect(const ListEquality().equals(bottomBarTypes, expectedTypes), isTrue);