diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index 5c66f9f..3d47495 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -607,17 +607,19 @@ class PlayerLogic extends ChangeNotifier { } Future openDetail(BuildContext context) async { - session.setRouteVisible(false); - await session.playbackSettled; + session.setOverlayOpen(true); + try { + if (!context.mounted) return; + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + DramaDetailPage(drama: queue.entryAt(session.currentIndex).drama), + ), + ); + } finally { + session.setOverlayOpen(false); + } await saveCurrentProgress(); - if (!context.mounted) return; - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => - DramaDetailPage(drama: queue.entryAt(session.currentIndex).drama), - ), - ); - if (isMounted()) session.setRouteVisible(active); } Future openSeriesAt( diff --git a/test/player/player_open_detail_test.dart b/test/player/player_open_detail_test.dart new file mode 100644 index 0000000..ae36b5b --- /dev/null +++ b/test/player/player_open_detail_test.dart @@ -0,0 +1,181 @@ +// Author: fruit.com.cn + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/data/models/drama.dart'; +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/android_picture_in_picture.dart'; +import 'package:fruitshorts/core/data/services/catalog_controller.dart'; +import 'package:fruitshorts/core/data/services/favorite_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/platform/haptics.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart'; +import 'package:fruitshorts/pages/player/player_logic.dart'; +import 'package:fruitshorts/pages/player/player_view.dart'; +import 'package:provider/provider.dart'; +import 'package:video_player/video_player.dart'; + +import '../support/fake_catalog.dart'; +import '../support/fake_haptics.dart'; +import '../support/fake_watch_history.dart'; + +void main() { + testWidgets( + 'should_keep_active_player_and_progress_when_detail_route_is_open', + (tester) async { + final drama = _drama(); + final history = FakeWatchHistory(); + final playback = FakePlaybackRepository(); + final catalog = CatalogController(FakeCatalogRepository(dramas: [drama])); + final favorites = FavoriteController( + InMemoryFavoriteRepository(saved: {drama.id}, cards: [drama]), + ); + final logic = PlayerLogic( + history: history, + playback: playback, + pictureInPicture: AndroidPictureInPicture( + channel: const MethodChannel('test/player_open_detail/pip'), + ), + ); + final factory = _RecordingFactory(); + logic.attach( + PlayerPage(drama: drama, initialIndex: 0), + isMounted: () => true, + ); + logic.start(factory: factory.create, coordinator: PlaybackCoordinator()); + await _pumpSession(tester); + + final active = logic.session.active; + final index = logic.session.currentIndex; + final player = factory.created.first; + final playsBefore = player.plays; + final tracked = VideoPlayerPort(drama.episodes.first); + tracked.controller.value = const VideoPlayerValue( + duration: Duration(minutes: 5), + position: Duration(seconds: 37), + isInitialized: true, + ); + logic + ..trackedPlayer = tracked + ..trackedEpisode = drama.episodes.first + ..trackedDrama = drama; + + late BuildContext pageContext; + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: history), + Provider.value(value: playback), + ChangeNotifierProvider.value(value: catalog), + ChangeNotifierProvider.value(value: favorites), + Provider.value(value: FakeHaptics()), + ], + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) { + pageContext = context; + return const SizedBox.shrink(); + }, + ), + ), + ), + ); + + unawaited(logic.openDetail(pageContext)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + await _pumpSession(tester); + + expect(find.byType(DramaDetailPage), findsOneWidget); + expect(logic.session.active, same(active)); + expect(logic.session.currentIndex, index); + expect(logic.session.userWantsPlay, isTrue); + expect(player.pauses, 1); + expect(player.plays, playsBefore); + expect(tracked.controller.value.position, const Duration(seconds: 37)); + expect(history.writes, isEmpty); + + await tester.pageBack(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + await _pumpSession(tester); + await logic.pendingWrite; + + expect(logic.session.active, same(active)); + expect(logic.session.currentIndex, index); + expect(logic.session.userWantsPlay, isTrue); + expect(player.plays, playsBefore + 1); + expect(tracked.controller.value.position, const Duration(seconds: 37)); + expect(history.writes, ['signal-1|37000|false']); + + logic + ..trackedPlayer = null + ..trackedEpisode = null + ..trackedDrama = null + ..dispose(); + await _pumpSession(tester); + await tracked.dispose(); + }, + ); +} + +Drama _drama() => Drama( + id: 'signal', + title: 'Signal', + synopsis: 'synopsis', + posterUrl: '/files/signal.png', + episodes: const [ + Episode( + id: 'signal-1', + number: 1, + title: 'Episode 1', + videoUrl: 'https://example.test/signal-1.mp4', + isFree: true, + ), + ], +); + +Future _pumpSession(WidgetTester tester) async { + for (var attempt = 0; attempt < 5; attempt++) { + await tester.pump(); + await tester.pump(const Duration(milliseconds: 1)); + } +} + +class _RecordingFactory { + final List<_RecordingPlayer> created = []; + + PlayerPort create(Episode episode) { + final player = _RecordingPlayer(); + created.add(player); + return player; + } +} + +class _RecordingPlayer implements PlayerPort { + int plays = 0; + int pauses = 0; + + @override + Future initialize() async {} + + @override + Future play() async => plays++; + + @override + Future pause() async => pauses++; + + @override + Future dispose() async {} +}