move favorite into button context

This commit is contained in:
timonrieger 2026-07-06 13:31:04 +02:00
parent 2fbbd7da58
commit 28336c8a7b
No known key found for this signature in database
3 changed files with 74 additions and 4 deletions

View file

@ -2,10 +2,9 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/actions/action.widget.dart';
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
import 'package:immich_mobile/providers/activity.provider.dart';
@ -14,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provid
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/routes.provider.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
import 'package:immich_mobile/utils/timezone.dart';
import 'package:immich_ui/immich_ui.dart';
@ -42,11 +42,24 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
final originalTheme = context.themeData;
final assetForAction = [asset];
// favorite only consumes `asset`/`isInLockedView`; other fields are unused placeholders.
final favoriteContext = ActionButtonContext(
asset: asset,
isOwner: false,
isArchived: false,
isTrashEnabled: false,
isStacked: false,
isInLockedView: isInLockedView,
currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.viewer,
);
final actions = <Widget>[
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
if (ActionButtonType.favorite.shouldShow(favoriteContext))
ActionButtonType.favorite.buildButton(favoriteContext, context, true),
ViewerKebabMenu(originalTheme: originalTheme),
];

View file

@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/presentation/actions/action.widget.dart';
import 'package:immich_mobile/presentation/actions/asset_debug.action.dart';
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
import 'package:immich_mobile/utils/semver.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';
@ -74,6 +75,7 @@ class ActionButtonContext {
enum ActionButtonType {
openInfo,
favorite,
openActivity,
likeActivity,
share,
@ -186,6 +188,9 @@ enum ActionButtonType {
context.asset is RemoteAsset && //
context.isOwner,
ActionButtonType.openInfo => true,
ActionButtonType.favorite =>
!context.isInLockedView && //
context.asset.hasRemote,
ActionButtonType.viewInTimeline =>
context.timelineOrigin != TimelineOrigin.main &&
context.timelineOrigin != TimelineOrigin.deepLink &&
@ -211,6 +216,7 @@ enum ActionButtonType {
ActionButtonType.openActivity || ActionButtonType.likeActivity =>
position != ButtonPosition.bottomBar || context.timelineOrigin != TimelineOrigin.trash,
ActionButtonType.editImage => position != ButtonPosition.bottomBar || context.currentAlbum?.isShared != true,
ActionButtonType.favorite => false,
_ => true,
};
@ -322,6 +328,10 @@ enum ActionButtonType {
ActionButtonType.editImage => EditImageActionButton(iconOnly: iconOnly, menuItem: menuItem),
ActionButtonType.addTo => AddActionButton(originalTheme: context.originalTheme),
ActionButtonType.openActivity => OpenActivityActionButton(iconOnly: iconOnly, menuItem: menuItem),
ActionButtonType.favorite =>
menuItem
? ActionMenuItemWidget(action: FavoriteAction(assets: [context.asset]))
: ActionIconButtonWidget(action: FavoriteAction(assets: [context.asset])),
};
}

View file

@ -4,6 +4,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/presentation/actions/action.widget.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
import 'package:immich_mobile/utils/semver.dart';
@ -1144,6 +1145,52 @@ void main() {
});
});
group('ActionButtonType.favorite', () {
ActionButtonContext contextFor(BaseAsset asset, {bool isInLockedView = false}) {
return ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: isInLockedView,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.viewer,
);
}
test('shouldShow is true for a remote asset outside locked view', () {
expect(ActionButtonType.favorite.shouldShow(contextFor(createRemoteAsset())), isTrue);
});
test('shouldShow is false in locked view', () {
expect(ActionButtonType.favorite.shouldShow(contextFor(createRemoteAsset(), isInLockedView: true)), isFalse);
});
test('shouldShow is false for a local-only asset', () {
expect(ActionButtonType.favorite.shouldShow(contextFor(createLocalAsset())), isFalse);
});
test('is excluded from the bottom bar and kebab menu positions', () {
final context = contextFor(createRemoteAsset());
expect(ActionButtonType.favorite.showsAt(ButtonPosition.bottomBar, context), isFalse);
expect(ActionButtonType.favorite.showsAt(ButtonPosition.kebabMenu, context), isFalse);
});
test('buildButton returns an icon button by default and a menu item for menuItem', () {
final context = contextFor(createRemoteAsset());
expect(ActionButtonType.favorite.buildButton(context), isA<ActionIconButtonWidget>());
expect(ActionButtonType.favorite.buildButton(context, null, false, true), isA<ActionMenuItemWidget>());
});
test('does not appear in the viewer bottom bar or kebab menu builders', () {
final context = contextFor(createRemoteAsset());
expect(ActionButtonBuilder.getViewerBottomBarTypes(context), isNot(contains(ActionButtonType.favorite)));
expect(ActionButtonBuilder.getViewerKebabMenuTypes(context), isNot(contains(ActionButtonType.favorite)));
});
});
group('ActionButtonBuilder', () {
test('should return buttons that should show', () {
final remoteAsset = createRemoteAsset();