diff --git a/mobile/lib/presentation/actions/action.dart b/mobile/lib/presentation/actions/action.dart index 0e6cfd2bd6..5ceb2f855d 100644 --- a/mobile/lib/presentation/actions/action.dart +++ b/mobile/lib/presentation/actions/action.dart @@ -28,5 +28,5 @@ abstract class AssetAction extends BaseAction { const AssetAction({required this.assets}); - Iterable filter(ActionScope scope); + Iterable filter(ActionScope scope) => assets.whereType(); } diff --git a/mobile/lib/presentation/actions/advanced_info.action.dart b/mobile/lib/presentation/actions/advanced_info.action.dart new file mode 100644 index 0000000000..6a4896fe34 --- /dev/null +++ b/mobile/lib/presentation/actions/advanced_info.action.dart @@ -0,0 +1,27 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class AdvancedInfoAction extends AssetAction { + const AdvancedInfoAction({required super.assets}); + + @override + IconData get icon => Icons.help_outline_rounded; + + @override + String label(ActionScope scope) => scope.context.t.troubleshoot; + + @override + bool isVisible(ActionScope scope) => + scope.ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting) && assets.length == 1; + + @override + Future onAction(ActionScope scope) async => + unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: assets.first))); +} 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 deleted file mode 100644 index b68ab69b26..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; - -class AdvancedInfoActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const AdvancedInfoActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - unawaited(ref.read(actionProvider.notifier).troubleshoot(source, context)); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - 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/asset_viewer/viewer_kebab_menu.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart index 418d41e1f2..da0abad1dd 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 @@ -12,6 +12,7 @@ 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_ui/immich_ui.dart'; class ViewerKebabMenu extends ConsumerWidget { const ViewerKebabMenu({super.key, this.originalTheme}); @@ -49,9 +50,9 @@ class ViewerKebabMenu extends ConsumerWidget { timelineOrigin: timelineOrigin, ); - final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref); + final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context); - return MenuAnchor( + return ImmichMenu( consumeOutsideTap: true, style: MenuStyle( backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor), @@ -62,7 +63,7 @@ class ViewerKebabMenu extends ConsumerWidget { ), padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)), ), - menuChildren: [ + children: [ ConstrainedBox( constraints: const BoxConstraints(minWidth: 150), child: Theme( diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index c3a569407a..2f4b007b83 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -3,8 +3,9 @@ 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/album/album.model.dart'; -import 'package:immich_mobile/domain/models/setting.model.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/advanced_info.action.dart'; +import 'package:immich_mobile/presentation/actions/timeline.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; @@ -24,7 +25,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_ import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -56,7 +56,6 @@ class _GeneralBottomSheetState extends ConsumerState { Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); - final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); final tagsEnabled = ref.watch( userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), ); @@ -84,6 +83,9 @@ class _GeneralBottomSheetState extends ConsumerState { return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } + final assets = multiselect.selectedAssets.toList(growable: false); + final actions = [AdvancedInfoAction(assets: assets)]; + return BaseBottomSheet( controller: sheetController, initialChildSize: widget.minChildSize ?? 0.15, @@ -91,9 +93,7 @@ class _GeneralBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ - if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[ - const AdvancedInfoActionButton(source: ActionSource.timeline), - ], + ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index b9cff613fd..8a86007c44 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -1,14 +1,14 @@ 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/album/album.model.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/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/advanced_info.action.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'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; @@ -185,18 +185,14 @@ enum ActionButtonType { }; } - ConsumerWidget buildButton( + Widget buildButton( ActionButtonContext context, [ BuildContext? buildContext, bool iconOnly = false, bool menuItem = false, ]) { return switch (this) { - ActionButtonType.advancedInfo => AdvancedInfoActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), + ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AdvancedInfoAction(assets: [context.asset])), ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.shareLink => ShareLinkActionButton( source: context.source, @@ -334,7 +330,7 @@ class ActionButtonBuilder { return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); } - static List buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext, WidgetRef ref) { + static List buildViewerKebabMenu(ActionButtonContext context, BuildContext buildContext) { final visibleButtons = defaultViewerKebabMenuOrder .where((type) => !defaultViewerBottomBarButtons.contains(type) && type.shouldShow(context)) .toList(); @@ -350,7 +346,7 @@ 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)); + result.add(type.buildButton(context, buildContext, false, true)); lastGroup = type.kebabMenuGroup; } diff --git a/mobile/test/unit/presentation/actions/advance_info_action_test.dart b/mobile/test/unit/presentation/actions/advance_info_action_test.dart new file mode 100644 index 0000000000..d721a3f798 --- /dev/null +++ b/mobile/test/unit/presentation/actions/advance_info_action_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/advanced_info.action.dart'; +import 'package:immich_ui/immich_ui.dart'; + +import '../../factories/remote_asset_factory.dart'; +import '../../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + await StoreService.I.put(StoreKey.advancedTroubleshooting, true); + }); + + tearDown(() { + context.dispose(); + }); + + group('AdvancedInfoAction', () { + testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async { + await tester.pumpTestWidget( + ActionIconButtonWidget(action: AdvancedInfoAction(assets: [RemoteAssetFactory.create()])), + overrides: context.overrides, + ); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('hidden for multiple assets', (tester) async { + await tester.pumpTestWidget( + ActionIconButtonWidget( + action: AdvancedInfoAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()]), + ), + overrides: context.overrides, + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('hidden when advanced troubleshooting is off', (tester) async { + await StoreService.I.put(StoreKey.advancedTroubleshooting, false); + await tester.pumpTestWidget( + ActionIconButtonWidget(action: AdvancedInfoAction(assets: [RemoteAssetFactory.create()])), + overrides: context.overrides, + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +}