fix(player): 修复首次双击点赞状态刷新
This commit is contained in:
1 parent
978c309cd7
commit
1b50a73bfc
3 files changed
+165
-13
No files matched your search
@@ -519,9 +519,22 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
Episode episode,
|
Episode episode,
|
||||||
) {
|
) {
|
||||||
_lastDoubleTapAt = DateTime.now();
|
_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));
|
unawaited(_toggleLike(context, interactions, episode, likeOnly: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _refreshLikeState() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
/// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。
|
/// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。
|
||||||
void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) {
|
void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -978,6 +991,9 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
'index=$index top=1 rail=1 progress=1',
|
'index=$index top=1 rail=1 progress=1',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (widget.homeMode && !_presentationActive && !theater) {
|
||||||
|
_logRailLikeState('home', episode.id, interactions);
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
@@ -1209,6 +1225,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
logic.session.preparedPlayerAt(index);
|
logic.session.preparedPlayerAt(index);
|
||||||
final videoPort = port is VideoPlayerPort ? port : null;
|
final videoPort = port is VideoPlayerPort ? port : null;
|
||||||
final videoReady = videoPort?.controller.value.isInitialized ?? false;
|
final videoReady = videoPort?.controller.value.isInitialized ?? false;
|
||||||
|
_logRailLikeState('player', episode.id, interactions);
|
||||||
return PlayerPlaybackOverlay(
|
return PlayerPlaybackOverlay(
|
||||||
data: PlayerOverlayData(
|
data: PlayerOverlayData(
|
||||||
l10n: l10n,
|
l10n: l10n,
|
||||||
@@ -1270,6 +1287,20 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<void> _presentFullPlayer(BuildContext context, int index) async {
|
Future<void> _presentFullPlayer(BuildContext context, int index) async {
|
||||||
if (_presentationActive ||
|
if (_presentationActive ||
|
||||||
_preparingFullPlayer ||
|
_preparingFullPlayer ||
|
||||||
|
|||||||
@@ -99,18 +99,62 @@ extension _PlayerViewActions on _PlayerPageState {
|
|||||||
Episode episode, {
|
Episode episode, {
|
||||||
bool likeOnly = false,
|
bool likeOnly = false,
|
||||||
}) async {
|
}) 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<Haptics>().lightImpact());
|
unawaited(context.read<Haptics>().lightImpact());
|
||||||
if (interactions.isEpisodeBusy(episode.id) ||
|
if (busyBefore || (likeOnly && likedBefore)) {
|
||||||
(likeOnly && interactions.isEpisodeLiked(episode.id))) {
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-like] phase=toggle-skipped '
|
||||||
|
'episodeId=${episode.id} '
|
||||||
|
'reason=${busyBefore ? 'busy' : 'already-liked'}',
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!await ensureSignedIn(context) || !context.mounted) return;
|
final signedIn = await ensureSignedIn(context);
|
||||||
if (interactions.isEpisodeBusy(episode.id) ||
|
if (kDebugMode) {
|
||||||
(likeOnly && interactions.isEpisodeLiked(episode.id))) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final result = await interactions.toggleEpisodeLike(episode.id);
|
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(
|
showCenteredToast(
|
||||||
context,
|
context,
|
||||||
result.errorMessage ?? AppLocalizations.of(context)!.likeFailed,
|
result.errorMessage ?? AppLocalizations.of(context)!.likeFailed,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/in_memory_shared_preferences_async.dart';
|
||||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.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/favorite_repository.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/playback_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/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/playback_session.dart';
|
||||||
import 'package:fruitshorts/core/data/services/video_player_port.dart';
|
import 'package:fruitshorts/core/data/services/video_player_port.dart';
|
||||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||||
@@ -655,7 +657,54 @@ void main() {
|
|||||||
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
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<InteractionController>(
|
||||||
|
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,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
SharedPreferencesAsyncPlatform.instance =
|
SharedPreferencesAsyncPlatform.instance =
|
||||||
@@ -665,7 +714,7 @@ void main() {
|
|||||||
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
||||||
});
|
});
|
||||||
final interactions = FakeInteractionRepository(
|
final interactions = FakeInteractionRepository(
|
||||||
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
|
nextLikeResult: const LikeToggleResult(liked: true, count: 23001),
|
||||||
);
|
);
|
||||||
await _pumpApp(tester, interactions: interactions);
|
await _pumpApp(tester, interactions: interactions);
|
||||||
await _openSeriesPlayer(tester);
|
await _openSeriesPlayer(tester);
|
||||||
@@ -673,18 +722,24 @@ void main() {
|
|||||||
final logic = _pageLogic(tester, find.byType(PlayerPage).last);
|
final logic = _pageLogic(tester, find.byType(PlayerPage).last);
|
||||||
final gesture = find.byKey(const Key('player-video-tap')).hitTestable();
|
final gesture = find.byKey(const Key('player-video-tap')).hitTestable();
|
||||||
final position = tester.getCenter(gesture);
|
final position = tester.getCenter(gesture);
|
||||||
|
final interactionController = Provider.of<InteractionController>(
|
||||||
|
tester.element(gesture),
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
expect(logic.session.userWantsPlay, isTrue);
|
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();
|
||||||
await tester.pump(const Duration(milliseconds: 300));
|
|
||||||
await _waitForBufferedHeartFrames(tester);
|
|
||||||
|
|
||||||
expect(logic.session.userWantsPlay, isTrue);
|
expect(logic.session.userWantsPlay, isTrue);
|
||||||
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
||||||
expect(interactions.likeTargets, ['EPISODE:city-lights-demo-1']);
|
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++) {
|
for (var index = 0; index < 3; index++) {
|
||||||
expect(find.byKey(Key('double-tap-heart-$index')), findsOneWidget);
|
expect(find.byKey(Key('double-tap-heart-$index')), findsOneWidget);
|
||||||
}
|
}
|
||||||
@@ -700,6 +755,15 @@ void main() {
|
|||||||
AppColors.doubleTapHeartRed,
|
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);
|
await _waitForHeartCompletion(tester);
|
||||||
expect(find.byKey(const Key('double-tap-heart-0')), findsNothing);
|
expect(find.byKey(const Key('double-tap-heart-0')), findsNothing);
|
||||||
expect(logic.session.userWantsPlay, isTrue);
|
expect(logic.session.userWantsPlay, isTrue);
|
||||||
@@ -1827,6 +1891,13 @@ Future<void> _waitForBufferedHeartFrames(WidgetTester tester) async {
|
|||||||
await tester.pump(const Duration(milliseconds: 120));
|
await tester.pump(const Duration(milliseconds: 120));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _waitForDoubleTapWindow(WidgetTester tester) async {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(milliseconds: 360)),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _waitForHeartCompletion(WidgetTester tester) async {
|
Future<void> _waitForHeartCompletion(WidgetTester tester) async {
|
||||||
await tester.runAsync(
|
await tester.runAsync(
|
||||||
() => Future<void>.delayed(const Duration(milliseconds: 1700)),
|
() => Future<void>.delayed(const Duration(milliseconds: 1700)),
|
||||||
@@ -1834,6 +1905,12 @@ Future<void> _waitForHeartCompletion(WidgetTester tester) async {
|
|||||||
await tester.pump(const Duration(milliseconds: 16));
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _railLabel(WidgetTester tester, Key key) => tester
|
||||||
|
.widget<Text>(
|
||||||
|
find.descendant(of: find.byKey(key), matching: find.byType(Text)),
|
||||||
|
)
|
||||||
|
.data!;
|
||||||
|
|
||||||
Future<void> _openSeriesPlayer(
|
Future<void> _openSeriesPlayer(
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
Key cardKey = const Key('home-feed-card-city-lights-demo'),
|
Key cardKey = const Key('home-feed-card-city-lights-demo'),
|
||||||
|
|||||||
Reference in new issue
Block a user