From 1b50a73bfceb18b49530d80a394c97a6a8f965e3 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 26 Oct 2026 19:17:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E4=BF=AE=E5=A4=8D=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E5=8F=8C=E5=87=BB=E7=82=B9=E8=B5=9E=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/player/player_view.dart | 31 ++++++++ lib/pages/player/player_view_actions.dart | 56 ++++++++++++-- test/player/player_fullscreen_test.dart | 91 +++++++++++++++++++++-- 3 files changed, 165 insertions(+), 13 deletions(-) diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 9b3a37f..c825021 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -519,9 +519,22 @@ class _PlayerPageState extends State Episode episode, ) { _lastDoubleTapAt = DateTime.now(); + if (kDebugMode) { + debugPrint( + '[player-like] phase=double-tap-callback ' + 'episodeId=${episode.id} ' + 'busy=${interactions.isEpisodeBusy(episode.id)} ' + 'liked=${interactions.isEpisodeLiked(episode.id)}', + ); + } unawaited(_toggleLike(context, interactions, episode, likeOnly: true)); } + void _refreshLikeState() { + if (!mounted) return; + setState(() {}); + } + /// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。 void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) { setState(() { @@ -978,6 +991,9 @@ class _PlayerPageState extends State 'index=$index top=1 rail=1 progress=1', ); } + if (widget.homeMode && !_presentationActive && !theater) { + _logRailLikeState('home', episode.id, interactions); + } return Stack( fit: StackFit.expand, @@ -1209,6 +1225,7 @@ class _PlayerPageState extends State logic.session.preparedPlayerAt(index); final videoPort = port is VideoPlayerPort ? port : null; final videoReady = videoPort?.controller.value.isInitialized ?? false; + _logRailLikeState('player', episode.id, interactions); return PlayerPlaybackOverlay( data: PlayerOverlayData( l10n: l10n, @@ -1270,6 +1287,20 @@ class _PlayerPageState extends State ); } + void _logRailLikeState( + String surface, + String episodeId, + InteractionController interactions, + ) { + if (!kDebugMode) return; + debugPrint( + '[player-like] phase=rail-state surface=$surface ' + 'episodeId=$episodeId ' + 'liked=${interactions.isEpisodeLiked(episodeId)} ' + 'count=${interactions.episodeLikeCount(episodeId)}', + ); + } + Future _presentFullPlayer(BuildContext context, int index) async { if (_presentationActive || _preparingFullPlayer || diff --git a/lib/pages/player/player_view_actions.dart b/lib/pages/player/player_view_actions.dart index e8af459..19f6667 100644 --- a/lib/pages/player/player_view_actions.dart +++ b/lib/pages/player/player_view_actions.dart @@ -99,18 +99,62 @@ extension _PlayerViewActions on _PlayerPageState { Episode episode, { bool likeOnly = false, }) async { + final busyBefore = interactions.isEpisodeBusy(episode.id); + final likedBefore = interactions.isEpisodeLiked(episode.id); + if (kDebugMode) { + debugPrint( + '[player-like] phase=toggle-start ' + 'episodeId=${episode.id} likeOnly=$likeOnly ' + 'busy=$busyBefore liked=$likedBefore', + ); + } unawaited(context.read().lightImpact()); - if (interactions.isEpisodeBusy(episode.id) || - (likeOnly && interactions.isEpisodeLiked(episode.id))) { + if (busyBefore || (likeOnly && likedBefore)) { + if (kDebugMode) { + debugPrint( + '[player-like] phase=toggle-skipped ' + 'episodeId=${episode.id} ' + 'reason=${busyBefore ? 'busy' : 'already-liked'}', + ); + } return; } - if (!await ensureSignedIn(context) || !context.mounted) return; - if (interactions.isEpisodeBusy(episode.id) || - (likeOnly && interactions.isEpisodeLiked(episode.id))) { + final signedIn = await ensureSignedIn(context); + if (kDebugMode) { + debugPrint( + '[player-like] phase=auth-result ' + 'episodeId=${episode.id} signedIn=$signedIn ' + 'mounted=${context.mounted}', + ); + } + if (!signedIn || !context.mounted) return; + final busyAfterAuth = interactions.isEpisodeBusy(episode.id); + final likedAfterAuth = interactions.isEpisodeLiked(episode.id); + if (busyAfterAuth || (likeOnly && likedAfterAuth)) { + if (kDebugMode) { + debugPrint( + '[player-like] phase=toggle-skipped-after-auth ' + 'episodeId=${episode.id} ' + 'reason=${busyAfterAuth ? 'busy' : 'already-liked'}', + ); + } return; } final result = await interactions.toggleEpisodeLike(episode.id); - if (result.isSuccess || !context.mounted) return; + if (kDebugMode) { + debugPrint( + '[player-like] phase=toggle-result ' + 'episodeId=${episode.id} success=${result.isSuccess} ' + 'liked=${interactions.isEpisodeLiked(episode.id)} ' + 'count=${interactions.episodeLikeCount(episode.id)} ' + 'error=${result.errorMessage ?? '-'}', + ); + } + if (result.isSuccess) { + _refreshLikeState(); + return; + } + if (!context.mounted) return; showCenteredToast( context, result.errorMessage ?? AppLocalizations.of(context)!.likeFailed, diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 854d6a8..784bb47 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; @@ -16,6 +17,7 @@ import 'package:fruitshorts/core/data/models/player_port.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/core/data/services/interaction_controller.dart'; import 'package:fruitshorts/core/data/services/playback_session.dart'; import 'package:fruitshorts/core/data/services/video_player_port.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; @@ -655,7 +657,54 @@ void main() { expect(find.byKey(const Key('player-play-toggle')), findsNothing); }); - testWidgets('should_double_tap_like_once_without_pausing_full_player', ( + testWidgets('should_like_once_on_first_home_feed_double_tap', (tester) async { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.withData({ + 'auth.accessToken': 'test-access', + 'auth.refreshToken': 'test-refresh', + 'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}', + }); + final interactions = FakeInteractionRepository( + nextLikeResult: const LikeToggleResult(liked: true, count: 23001), + ); + await _pumpApp(tester, interactions: interactions); + await tester.tap(find.byKey(const Key('tab-recommend'))); + await tester.pumpAndSettle(); + + final page = find.byKey(const Key('recommend-feed-player')); + final logic = _pageLogic(tester, page); + final episode = logic.queue.entryAt(logic.session.currentIndex).episode; + final gesture = find.byKey(const Key('home-feed-gesture')).hitTestable(); + final position = tester.getCenter(gesture); + final interactionController = Provider.of( + tester.element(gesture), + listen: false, + ); + + await tester.tapAt(position); + await tester.tapAt(position); + await tester.pump(); + await tester.pump(); + + expect(interactionController.isEpisodeLiked(episode.id), isTrue); + expect(interactionController.episodeLikeCount(episode.id), 23001); + expect(_railLabel(tester, const Key('rail-like')), '2.3万'); + expect(logic.session.userWantsPlay, isTrue); + expect(interactions.likeTargets, ['EPISODE:${episode.id}']); + + await _waitForDoubleTapWindow(tester); + await tester.tapAt(position); + await tester.tapAt(position); + await tester.pump(); + await tester.pump(); + + expect(interactions.likeTargets, hasLength(1)); + expect(interactionController.episodeLikeCount(episode.id), 23001); + expect(logic.session.userWantsPlay, isTrue); + await _waitForHeartCompletion(tester); + }); + + testWidgets('should_like_once_on_first_double_tap_without_pausing', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = @@ -665,7 +714,7 @@ void main() { 'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}', }); final interactions = FakeInteractionRepository( - nextLikeResult: const LikeToggleResult(liked: true, count: 23000), + nextLikeResult: const LikeToggleResult(liked: true, count: 23001), ); await _pumpApp(tester, interactions: interactions); await _openSeriesPlayer(tester); @@ -673,18 +722,24 @@ void main() { final logic = _pageLogic(tester, find.byType(PlayerPage).last); final gesture = find.byKey(const Key('player-video-tap')).hitTestable(); final position = tester.getCenter(gesture); + final interactionController = Provider.of( + tester.element(gesture), + listen: false, + ); expect(logic.session.userWantsPlay, isTrue); - for (var index = 0; index < 4; index++) { - await tester.tapAt(position); - } + await tester.tapAt(position); + await tester.tapAt(position); + await tester.pump(); await tester.pump(); - await tester.pump(const Duration(milliseconds: 300)); - await _waitForBufferedHeartFrames(tester); expect(logic.session.userWantsPlay, isTrue); expect(find.byKey(const Key('player-play-toggle')), findsNothing); expect(interactions.likeTargets, ['EPISODE:city-lights-demo-1']); + expect(interactionController.isEpisodeLiked('city-lights-demo-1'), isTrue); + expect(interactionController.episodeLikeCount('city-lights-demo-1'), 23001); + expect(_railLabel(tester, const Key('player-rail-like')), '2.3万'); + await _waitForBufferedHeartFrames(tester); for (var index = 0; index < 3; index++) { expect(find.byKey(Key('double-tap-heart-$index')), findsOneWidget); } @@ -700,6 +755,15 @@ void main() { AppColors.doubleTapHeartRed, ); + await _waitForDoubleTapWindow(tester); + await tester.tapAt(position); + await tester.tapAt(position); + await tester.pump(); + await tester.pump(); + + expect(interactions.likeTargets, hasLength(1)); + expect(interactionController.episodeLikeCount('city-lights-demo-1'), 23001); + expect(logic.session.userWantsPlay, isTrue); await _waitForHeartCompletion(tester); expect(find.byKey(const Key('double-tap-heart-0')), findsNothing); expect(logic.session.userWantsPlay, isTrue); @@ -1827,6 +1891,13 @@ Future _waitForBufferedHeartFrames(WidgetTester tester) async { await tester.pump(const Duration(milliseconds: 120)); } +Future _waitForDoubleTapWindow(WidgetTester tester) async { + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 360)), + ); + await tester.pump(); +} + Future _waitForHeartCompletion(WidgetTester tester) async { await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 1700)), @@ -1834,6 +1905,12 @@ Future _waitForHeartCompletion(WidgetTester tester) async { await tester.pump(const Duration(milliseconds: 16)); } +String _railLabel(WidgetTester tester, Key key) => tester + .widget( + find.descendant(of: find.byKey(key), matching: find.byType(Text)), + ) + .data!; + Future _openSeriesPlayer( WidgetTester tester, { Key cardKey = const Key('home-feed-card-city-lights-demo'),