refactor: mobile cast and slideshow action

This commit is contained in:
shenlong-tanwen
2026-07-27 15:08:54 +05:30
parent 4ed7b7a288
commit 290e7fd4b5
6 changed files with 100 additions and 68 deletions
@@ -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())),
);
}
}
@@ -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)))),
);
}
@@ -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,
);
}
}
@@ -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,
);
}
}
+4 -4
View File
@@ -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()),
};
}
@@ -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);
});
});
}