From f613033088aa951e7c80e93b56ccb4aa717a7b4f Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 16:43:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E6=94=B9=E4=B8=BA=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E6=B8=85=E5=B1=8F=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_id.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_zh.arb | 1 + lib/pages/player/player_view.dart | 208 ++++++++---------- .../player/widgets/home_feed_overlay.dart | 9 + test/home/home_preview_layout_test.dart | 1 + test/player/player_center_toggle_test.dart | 8 +- test/player/player_fullscreen_test.dart | 80 +++---- 12 files changed, 158 insertions(+), 166 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 74e71e8..ccb0f0b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -46,6 +46,7 @@ "total": {"type": "int"} } }, + "clearScreen": "Clear screen", "minutesAgo": "{count} min ago", "@minutesAgo": { "placeholders": { diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 66274c6..eebf31f 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -46,6 +46,7 @@ "total": {"type": "int"} } }, + "clearScreen": "Bersihkan layar", "minutesAgo": "{count} mnt lalu", "@minutesAgo": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9d9ecb0..ee63cd3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -340,6 +340,12 @@ abstract class AppLocalizations { /// **'Episode {current} / {total}'** String episodeOfTotal(int current, int total); + /// No description provided for @clearScreen. + /// + /// In en, this message translates to: + /// **'Clear screen'** + String get clearScreen; + /// No description provided for @minutesAgo. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0fd6034..dbafe08 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -133,6 +133,9 @@ class AppLocalizationsEn extends AppLocalizations { return 'Episode $current / $total'; } + @override + String get clearScreen => 'Clear screen'; + @override String minutesAgo(int count) { return '$count min ago'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 4947298..02e2e01 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -133,6 +133,9 @@ class AppLocalizationsId extends AppLocalizations { return 'Episode $current / $total'; } + @override + String get clearScreen => 'Bersihkan layar'; + @override String minutesAgo(int count) { return '$count mnt lalu'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6010528..41821ad 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -130,6 +130,9 @@ class AppLocalizationsZh extends AppLocalizations { return '第 $current 集 / 共 $total 集'; } + @override + String get clearScreen => '清屏'; + @override String minutesAgo(int count) { return '$count 分钟前'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 493979a..88d2dfd 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -46,6 +46,7 @@ "total": {"type": "int"} } }, + "clearScreen": "清屏", "minutesAgo": "{count} 分钟前", "@minutesAgo": { "placeholders": { diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 7649661..49edc2a 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -30,16 +30,6 @@ import 'widgets/seek_progress_bar.dart'; import '../../core/data/repositories/watch_history_repository.dart'; import '../../core/data/repositories/playback_repository.dart'; -/// The home feed has no overlay play button, so it keeps its own center hint. -/// Detail/series playback uses `_PlaybackOverlay.player-play-toggle` instead. -bool shouldShowFeedPauseIndicator({ - required bool homeMode, - required bool selected, - required bool ready, - required bool playing, - required bool pip, -}) => homeMode && selected && ready && !playing && !pip; - class PlayerPage extends StatefulWidget { const PlayerPage({ super.key, @@ -80,6 +70,7 @@ class PlayerPage extends StatefulWidget { class _PlayerPageState extends State with WidgetsBindingObserver { late final PlayerLogic logic; + bool _feedChromeVisible = true; @override void initState() { @@ -198,9 +189,10 @@ class _PlayerPageState extends State with WidgetsBindingObserver { children: [ const ColoredBox(color: AppColors.ink), ?video, - if (!logic.chromeHidden) - const IgnorePointer( - child: DecoratedBox( + if (!logic.chromeHidden && (!widget.homeMode || _feedChromeVisible)) + IgnorePointer( + key: widget.homeMode ? const Key('home-feed-scrim') : null, + child: const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -312,67 +304,59 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ), ), ), - if (shouldShowFeedPauseIndicator( - homeMode: widget.homeMode, - selected: selected, - ready: ready, - playing: logic.session.userWantsPlay, - pip: logic.pip, - )) - Center( - child: PlayerCenterToggle( - key: const Key('home-feed-center-play'), - playing: false, - onTap: () => logic.session.setUserWantsPlay(true), - ), - ), if (logic.pip || logic.pipPreparing) const SizedBox.expand() else if (widget.homeMode) - HomeFeedOverlay( - l10n: l10n, - drama: drama, - episode: episode, - liked: interactions.isLiked(drama.id), - saved: context.watch().isSaved(drama.id), - onLike: () async { - if (await ensureSignedIn(context)) { - if (!context.mounted) return; - unawaited(context.read().lightImpact()); - final result = await interactions.toggleDramaLike(drama.id); - if (!result.isSuccess && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - result.errorMessage ?? - AppLocalizations.of(context)!.likeFailed, - ), - ), - ); - } - } - }, - onSave: () async { - final favorites = context.read(); - if (await ensureSignedIn(context)) { - if (!context.mounted) return; - unawaited(context.read().lightImpact()); - final ok = await favorites.toggle(drama.id, card: drama); - if (ok || !context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.favoriteToggleFailed)), - ); - } - }, - onComments: () => logic.showComments(context, drama), - onShare: () => logic.shareDrama(context, drama), - onEpisodes: () => logic.showEpisodes(context), - onDownload: () => requestEpisodeDownload(context, episode.id), - onSearch: widget.onSearch ?? () => logic.showDiscover(context), - onPip: logic.pictureInPicture.isPlatformSupported - ? () => unawaited(logic.enterPip(context)) - : null, - ) + _feedChromeVisible + ? HomeFeedOverlay( + l10n: l10n, + drama: drama, + episode: episode, + liked: interactions.isLiked(drama.id), + saved: context.watch().isSaved(drama.id), + onLike: () async { + if (await ensureSignedIn(context)) { + if (!context.mounted) return; + unawaited(context.read().lightImpact()); + final result = await interactions.toggleDramaLike( + drama.id, + ); + if (!result.isSuccess && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + result.errorMessage ?? + AppLocalizations.of(context)!.likeFailed, + ), + ), + ); + } + } + }, + onSave: () async { + final favorites = context.read(); + if (await ensureSignedIn(context)) { + if (!context.mounted) return; + unawaited(context.read().lightImpact()); + final ok = await favorites.toggle(drama.id, card: drama); + if (ok || !context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.favoriteToggleFailed)), + ); + } + }, + onComments: () => logic.showComments(context, drama), + onShare: () => logic.shareDrama(context, drama), + onEpisodes: () => logic.showEpisodes(context), + onDownload: () => requestEpisodeDownload(context, episode.id), + onClear: () => setState(() => _feedChromeVisible = false), + onSearch: + widget.onSearch ?? () => logic.showDiscover(context), + onPip: logic.pictureInPicture.isPlatformSupported + ? () => unawaited(logic.enterPip(context)) + : null, + ) + : const SizedBox.shrink() else _PlaybackOverlay( l10n: l10n, @@ -433,6 +417,14 @@ class _PlayerPageState extends State with WidgetsBindingObserver { ? (position) => logic.seekTo(context, videoPort!, position) : null, ), + if (widget.homeMode && !_feedChromeVisible) + Positioned.fill( + child: GestureDetector( + key: const Key('home-feed-video-tap'), + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => _feedChromeVisible = true), + ), + ), ], ); } @@ -608,50 +600,17 @@ class _PlaybackOverlay extends StatefulWidget { } class _PlaybackOverlayState extends State<_PlaybackOverlay> { - Timer? _hideTimer; bool _controlsVisible = true; - @override - void initState() { - super.initState(); - _scheduleHide(); - } - - @override - void dispose() { - _hideTimer?.cancel(); - super.dispose(); - } - - void _scheduleHide() { - _hideTimer?.cancel(); - _hideTimer = Timer(const Duration(milliseconds: 2500), () { - if (mounted) setState(() => _controlsVisible = false); - }); - } - - void _reveal() { - setState(() => _controlsVisible = true); - _scheduleHide(); - } - - void _togglePlay() { - widget.onTogglePlay(); - _reveal(); - } - void _onVideoTap() { if (!_controlsVisible) { - _reveal(); - } else { - _togglePlay(); + setState(() => _controlsVisible = true); + } else if (!widget.playing) { + widget.onTogglePlay(); } } - void _run(VoidCallback action) { - action(); - _reveal(); - } + void _clearScreen() => setState(() => _controlsVisible = false); @override Widget build(BuildContext context) { @@ -673,13 +632,14 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { Expanded( child: Stack( children: [ - Center( - child: PlayerCenterToggle( - key: const Key('player-play-toggle'), - playing: widget.playing, - onTap: _togglePlay, + if (widget.playing) + Center( + child: PlayerCenterToggle( + key: const Key('player-play-toggle'), + playing: true, + onTap: widget.onTogglePlay, + ), ), - ), const Positioned( right: 10, bottom: 14, @@ -715,7 +675,7 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { key: const Key('player-episodes'), icon: Icons.view_list_rounded, label: widget.l10n.episodes, - onTap: () => _run(widget.onEpisodes), + onTap: widget.onEpisodes, ), ), const SizedBox(width: 8), @@ -724,7 +684,7 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { key: const Key('player-comments'), icon: Icons.chat_bubble_outline_rounded, label: widget.l10n.comments, - onTap: () => _run(widget.onComments), + onTap: widget.onComments, ), ), const SizedBox(width: 8), @@ -733,9 +693,7 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { height: 32, child: FilledButton( key: const Key('player-next'), - onPressed: widget.onNext == null - ? null - : () => _run(widget.onNext!), + onPressed: widget.onNext, style: FilledButton.styleFrom( backgroundColor: AppColors.blue, disabledBackgroundColor: AppColors.raised, @@ -775,7 +733,7 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { const SizedBox(height: 12), PlayerFeaturedComment( drama: widget.drama, - onViewAll: () => _run(widget.onComments), + onViewAll: widget.onComments, ), ], ), @@ -800,7 +758,7 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { key: const Key('player-back'), tooltip: widget.l10n.backToSeries, icon: Icons.chevron_left_rounded, - onPressed: () => _run(widget.onBack!), + onPressed: widget.onBack!, ) else const SizedBox(width: 36), @@ -822,6 +780,13 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { ), ), const SizedBox(width: 8), + _TopAction( + key: const Key('player-clear-screen'), + tooltip: widget.l10n.clearScreen, + icon: Icons.fullscreen_rounded, + onPressed: _clearScreen, + ), + const SizedBox(width: 8), _TopAction( key: const Key('player-more'), tooltip: widget.l10n.more, @@ -907,7 +872,6 @@ class _PlaybackOverlayState extends State<_PlaybackOverlay> { ), ), ); - if (mounted) _reveal(); } } diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 30eadf0..292d4ed 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -26,6 +26,7 @@ class HomeFeedOverlay extends StatelessWidget { required this.onShare, required this.onEpisodes, required this.onDownload, + required this.onClear, required this.onSearch, this.onPip, }); @@ -41,6 +42,7 @@ class HomeFeedOverlay extends StatelessWidget { final VoidCallback onShare; final VoidCallback onEpisodes; final VoidCallback onDownload; + final VoidCallback onClear; final VoidCallback onSearch; final VoidCallback? onPip; @@ -96,6 +98,13 @@ class HomeFeedOverlay extends StatelessWidget { onPressed: onSearch, ), const SizedBox(width: 8), + _RoundIcon( + key: const Key('home-feed-clear-screen'), + tooltip: l10n.clearScreen, + icon: Icons.fullscreen_rounded, + onPressed: onClear, + ), + const SizedBox(width: 8), _RoundIcon( key: const Key('home-feed-more'), tooltip: l10n.more, diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index c7428f1..df22998 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -101,6 +101,7 @@ void main() { onShare: () {}, onEpisodes: () {}, onDownload: () {}, + onClear: () {}, onSearch: () {}, ), ), diff --git a/test/player/player_center_toggle_test.dart b/test/player/player_center_toggle_test.dart index 1e3a0ae..e6912cd 100644 --- a/test/player/player_center_toggle_test.dart +++ b/test/player/player_center_toggle_test.dart @@ -5,16 +5,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart'; void main() { - testWidgets('should_use_one_shared_52px_style_for_home_and_player', ( - tester, - ) async { + testWidgets('should_use_one_shared_52px_center_toggle_style', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Row( children: [ PlayerCenterToggle( - key: const Key('home-feed-center-play'), + key: const Key('test-center-toggle-a'), playing: false, onTap: () {}, ), @@ -30,7 +28,7 @@ void main() { ); for (final key in const [ - Key('home-feed-center-play'), + Key('test-center-toggle-a'), Key('player-play-toggle'), ]) { expect(tester.getSize(find.byKey(key)), const Size(52, 52)); diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 7f71cad..21db0c5 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -9,7 +9,6 @@ import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; -import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart'; import '../support/fake_catalog.dart'; @@ -70,7 +69,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('should_keep_single_pause_toggle_when_detail_player_is_paused', ( + testWidgets('should_hide_pause_toggle_when_playback_is_paused', ( tester, ) async { await _pumpApp(tester); @@ -86,12 +85,16 @@ void main() { await tester.tap(find.byKey(const Key('player-play-toggle'))); await tester.pumpAndSettle(); + expect(find.byKey(const Key('player-play-toggle')), findsNothing); + expect(find.byKey(const Key('player-controls-top')), findsOneWidget); + expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); + + await tester.tap(find.byKey(const Key('player-video-tap'))); + await tester.pump(); expect(find.byKey(const Key('player-play-toggle')), findsOneWidget); - expect(find.byKey(const Key('home-feed-center-play')), findsNothing); - expect(find.byIcon(Icons.play_arrow_rounded), findsOneWidget); }); - testWidgets('should_hide_controls_after_2_5s_and_reveal_on_video_tap', ( + testWidgets('should_keep_controls_visible_until_clear_screen_is_tapped', ( tester, ) async { await _pumpApp(tester); @@ -108,7 +111,14 @@ void main() { expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); expect(find.byType(PlayerCenterToggle), findsOneWidget); - await tester.pump(const Duration(milliseconds: 2600)); + await tester.pump(const Duration(seconds: 5)); + await tester.pump(); + + expect(find.byKey(const Key('player-controls-top')), findsOneWidget); + expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget); + expect(find.byType(PlayerCenterToggle), findsOneWidget); + + await tester.tap(find.byKey(const Key('player-clear-screen'))); await tester.pump(); expect(find.byKey(const Key('player-controls-top')), findsNothing); @@ -124,39 +134,6 @@ void main() { expect(find.byType(PlayerCenterToggle), findsOneWidget); }); - test('should_only_show_feed_pause_indicator_in_home_mode', () { - expect( - shouldShowFeedPauseIndicator( - homeMode: true, - selected: true, - ready: true, - playing: false, - pip: false, - ), - isTrue, - ); - expect( - shouldShowFeedPauseIndicator( - homeMode: false, - selected: true, - ready: true, - playing: false, - pip: false, - ), - isFalse, - ); - expect( - shouldShowFeedPauseIndicator( - homeMode: true, - selected: true, - ready: true, - playing: true, - pip: false, - ), - isFalse, - ); - }); - testWidgets('should_fit_player_header_on_320_width', (tester) async { await _pumpApp(tester, size: const Size(320, 568)); await tester.binding.handlePopRoute(); @@ -174,6 +151,31 @@ void main() { ); expect(tester.takeException(), isNull); }); + + testWidgets('should_clear_and_restore_home_feed_chrome', (tester) async { + await _pumpApp(tester); + await tester.tap(find.byKey(const Key('tab-home'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + expect(find.byKey(const Key('home-feed-info-card')), findsOneWidget); + expect(find.byKey(const Key('home-feed-scrim')), findsOneWidget); + + await tester.tap(find.byKey(const Key('home-feed-clear-screen'))); + await tester.pump(); + + expect(find.byKey(const Key('home-feed-title')), findsNothing); + expect(find.byKey(const Key('home-feed-info-card')), findsNothing); + expect(find.byKey(const Key('home-feed-scrim')), findsNothing); + expect(find.byKey(const Key('home-feed-video-tap')), findsOneWidget); + + await tester.tap(find.byKey(const Key('home-feed-video-tap'))); + await tester.pump(); + + expect(find.byKey(const Key('home-feed-title')), findsOneWidget); + expect(find.byKey(const Key('home-feed-info-card')), findsOneWidget); + expect(find.byKey(const Key('home-feed-scrim')), findsOneWidget); + }); } void _expectFullscreenOverlay(WidgetTester tester) {