From 290e7fd4b50a0ab87427e0fff6cb3f0f68fc93ca Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:32:34 +0530 Subject: [PATCH] refactor: mobile cast and slideshow action --- .../lib/presentation/actions/cast.action.dart | 23 ++++++++ .../actions/slideshow.action.dart | 21 ++++++++ .../cast_action_button.widget.dart | 30 ----------- .../slideshow_action_button.widget.dart | 34 ------------ mobile/lib/utils/action_button.utils.dart | 8 +-- .../actions/cast_action_test.dart | 52 +++++++++++++++++++ 6 files changed, 100 insertions(+), 68 deletions(-) create mode 100644 mobile/lib/presentation/actions/cast.action.dart create mode 100644 mobile/lib/presentation/actions/slideshow.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/cast_action_test.dart diff --git a/mobile/lib/presentation/actions/cast.action.dart b/mobile/lib/presentation/actions/cast.action.dart new file mode 100644 index 0000000000..9ae47f44ef --- /dev/null +++ b/mobile/lib/presentation/actions/cast.action.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; + +class CastAction extends ActionBuilder { + const CastAction(); + + @override + ActionData build(BuildContext context, WidgetRef ref) { + final isCasting = ref.watch(castProvider.select((state) => state.isCasting)); + + return .new( + icon: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + label: context.t.cast, + onAction: () async => unawaited(showDialog(context: context, builder: (_) => const CastDialog())), + ); + } +} diff --git a/mobile/lib/presentation/actions/slideshow.action.dart b/mobile/lib/presentation/actions/slideshow.action.dart new file mode 100644 index 0000000000..967c46b6f2 --- /dev/null +++ b/mobile/lib/presentation/actions/slideshow.action.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SlideshowAction extends ActionBuilder { + const SlideshowAction(); + + @override + ActionData build(BuildContext context, WidgetRef ref) => .new( + icon: Icons.slideshow, + label: context.t.slideshow, + onAction: () async => + unawaited(context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider)))), + ); +} diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart deleted file mode 100644 index 7a4f84fb4f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.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/cast.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; - -class CastActionButton extends ConsumerWidget { - const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - - final bool iconOnly; - final bool menuItem; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - return BaseActionButton( - iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - iconColor: isCasting ? context.primaryColor : null, // null = default color - label: "cast".t(context: context), - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, - iconOnly: iconOnly, - menuItem: menuItem, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart deleted file mode 100644 index 479cf2dfe9..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.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/timeline.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SlideshowActionButton extends ConsumerWidget { - final bool iconOnly; - final bool menuItem; - - const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - - void _onTap(BuildContext context, WidgetRef ref) { - if (!context.mounted) { - return; - } - - context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.slideshow, - label: "slideshow".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 32aaf360d0..41015fb064 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -10,22 +10,22 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; +import 'package:immich_mobile/presentation/actions/cast.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; +import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.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'; 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/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -179,7 +179,7 @@ enum ActionButtonType { iconOnly: iconOnly, menuItem: menuItem, ), - ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.slideshow => const ActionMenuItem(action: SlideshowAction()), ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), @@ -228,7 +228,7 @@ enum ActionButtonType { EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt)); }, ), - ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.cast => const ActionMenuItem(action: CastAction()), }; } diff --git a/mobile/test/unit/presentation/actions/cast_action_test.dart b/mobile/test/unit/presentation/actions/cast_action_test.dart new file mode 100644 index 0000000000..45f1a842f5 --- /dev/null +++ b/mobile/test/unit/presentation/actions/cast_action_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/cast.action.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + }); + + tearDown(() { + context.dispose(); + }); + + void Function(bool) captureConnectionListener() => + verify(() => context.service.cast.onConnectionState = captureAny()).captured.single as void Function(bool); + + group('CastAction', () { + testWidgets('offers to cast when nothing is connected', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + expect(find.byIcon(Icons.cast_rounded), findsOneWidget); + }); + + testWidgets('switches to the connected icon once casting starts', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + captureConnectionListener()(true); + await tester.pump(); + + expect(find.byIcon(Icons.cast_connected_rounded), findsOneWidget); + expect(find.byIcon(Icons.cast_rounded), findsNothing); + }); + + testWidgets('switches back when casting stops', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + final onConnectionState = captureConnectionListener(); + onConnectionState(true); + await tester.pump(); + onConnectionState(false); + await tester.pump(); + + expect(find.byIcon(Icons.cast_rounded), findsOneWidget); + }); + }); +}