fix(player): 修复切集返回推荐页黑屏
This commit is contained in:
1 parent
9d2d3b97a1
commit
440188f261
5 files changed
+297
-37
No files matched your search
@@ -4,6 +4,9 @@ import '../models/drama.dart';
|
|||||||
|
|
||||||
/// A player session has one navigation meaning: browse dramas or browse episodes.
|
/// A player session has one navigation meaning: browse dramas or browse episodes.
|
||||||
class PlaybackQueue {
|
class PlaybackQueue {
|
||||||
|
PlaybackQueue.fromEntries(List<PlaybackEntry> entries)
|
||||||
|
: entries = List.unmodifiable(entries);
|
||||||
|
|
||||||
PlaybackQueue.home(List<Drama> dramas)
|
PlaybackQueue.home(List<Drama> dramas)
|
||||||
: entries = List.unmodifiable(
|
: entries = List.unmodifiable(
|
||||||
dramas
|
dramas
|
||||||
|
|||||||
@@ -137,6 +137,47 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
if (_visible && !_overlayOpen) _warmNeighbors(this);
|
if (_visible && !_overlayOpen) _warmNeighbors(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 在剧维度和集维度之间切换队列,但保留当前 active controller 与播放状态。
|
||||||
|
void remapQueuePreservingActive(
|
||||||
|
List<Episode> nextEpisodes, {
|
||||||
|
required int currentIndex,
|
||||||
|
int? preloadIndex,
|
||||||
|
int? previousIndex,
|
||||||
|
bool notify = true,
|
||||||
|
}) {
|
||||||
|
if (_closed ||
|
||||||
|
currentIndex < 0 ||
|
||||||
|
currentIndex >= nextEpisodes.length ||
|
||||||
|
_active == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_generation++;
|
||||||
|
episodes
|
||||||
|
..clear()
|
||||||
|
..addAll(nextEpisodes);
|
||||||
|
_active!.index = currentIndex;
|
||||||
|
_currentIndex = currentIndex;
|
||||||
|
_selected = true;
|
||||||
|
_loading = false;
|
||||||
|
_error = null;
|
||||||
|
_rememberNeighbors(preloadIndex, previousIndex);
|
||||||
|
final staleNext = _next;
|
||||||
|
final stalePrevious = _previous;
|
||||||
|
final staleHandoff = _handoff;
|
||||||
|
_next = null;
|
||||||
|
_previous = null;
|
||||||
|
_handoff = null;
|
||||||
|
if (notify) _notify();
|
||||||
|
unawaited(
|
||||||
|
_enqueue(() async {
|
||||||
|
await _retire(staleNext);
|
||||||
|
await _retire(stalePrevious);
|
||||||
|
await _retire(staleHandoff);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (_visible && !_overlayOpen) _warmNeighbors(this);
|
||||||
|
}
|
||||||
|
|
||||||
void _rememberNeighbors(int? preloadIndex, int? previousIndex) {
|
void _rememberNeighbors(int? preloadIndex, int? previousIndex) {
|
||||||
_nextIndex = preloadIndex;
|
_nextIndex = preloadIndex;
|
||||||
_previousIndex = previousIndex;
|
_previousIndex = previousIndex;
|
||||||
|
|||||||
@@ -212,6 +212,68 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
unawaited(selectEpisode(initialIndex));
|
unawaited(selectEpisode(initialIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 独立播放页把 feed 的“剧索引”切换成 series 的“集索引”,保留 active player。
|
||||||
|
void adoptSeriesHandoff({
|
||||||
|
required Drama drama,
|
||||||
|
required List<Episode> seriesEpisodes,
|
||||||
|
required int episodeIndex,
|
||||||
|
}) {
|
||||||
|
if (episodeIndex < 0 || episodeIndex >= seriesEpisodes.length) return;
|
||||||
|
homeMode = false;
|
||||||
|
embedded = false;
|
||||||
|
feedDramas = null;
|
||||||
|
pageDrama = drama;
|
||||||
|
initialIndex = episodeIndex;
|
||||||
|
queue = PlaybackQueue.series(drama);
|
||||||
|
visibleIndices = List<int>.generate(queue.length, (i) => i);
|
||||||
|
sessionEpisodes
|
||||||
|
..clear()
|
||||||
|
..addAll(seriesEpisodes);
|
||||||
|
session.remapQueuePreservingActive(
|
||||||
|
List<Episode>.of(seriesEpisodes),
|
||||||
|
currentIndex: episodeIndex,
|
||||||
|
notify: false,
|
||||||
|
preloadIndex: episodeIndex + 1 < seriesEpisodes.length
|
||||||
|
? episodeIndex + 1
|
||||||
|
: null,
|
||||||
|
previousIndex: episodeIndex > 0 ? episodeIndex - 1 : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返回推荐页时恢复剧维度 queue,并把当前集写回该剧的 feed 条目。
|
||||||
|
void restoreFeedHandoff({
|
||||||
|
required PlayerPage page,
|
||||||
|
required PlaybackQueue sourceQueue,
|
||||||
|
required List<Episode> sourceEpisodes,
|
||||||
|
required int sourceIndex,
|
||||||
|
}) {
|
||||||
|
if (sourceIndex < 0 || sourceIndex >= sourceQueue.length) return;
|
||||||
|
final currentEpisode = session.currentEpisode;
|
||||||
|
final feedEpisodes = List<Episode>.of(sourceEpisodes);
|
||||||
|
feedEpisodes[sourceIndex] = currentEpisode;
|
||||||
|
final entries = List<PlaybackEntry>.of(sourceQueue.entries);
|
||||||
|
entries[sourceIndex] = PlaybackEntry(
|
||||||
|
entries[sourceIndex].drama,
|
||||||
|
currentEpisode,
|
||||||
|
);
|
||||||
|
feedDramas = page.feedDramas;
|
||||||
|
pageDrama = page.drama;
|
||||||
|
homeMode = page.homeMode;
|
||||||
|
embedded = page.embedded;
|
||||||
|
initialIndex = sourceIndex;
|
||||||
|
queue = PlaybackQueue.fromEntries(entries);
|
||||||
|
visibleIndices = List<int>.generate(queue.length, (i) => i);
|
||||||
|
sessionEpisodes
|
||||||
|
..clear()
|
||||||
|
..addAll(feedEpisodes);
|
||||||
|
session.remapQueuePreservingActive(
|
||||||
|
List<Episode>.of(feedEpisodes),
|
||||||
|
currentIndex: sourceIndex,
|
||||||
|
preloadIndex: nextVisibleIndex(sourceIndex),
|
||||||
|
previousIndex: previousVisibleIndex(sourceIndex),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 串行替换首页 feed 队列;频道变化时重置到第 1 个,刷新时尽量保留当前剧。
|
/// 串行替换首页 feed 队列;频道变化时重置到第 1 个,刷新时尽量保留当前剧。
|
||||||
void updateFeed(
|
void updateFeed(
|
||||||
List<Drama> dramas, {
|
List<Drama> dramas, {
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../core/config/app_config.dart';
|
import '../../core/config/app_config.dart';
|
||||||
import '../../core/data/models/drama.dart';
|
import '../../core/data/models/drama.dart';
|
||||||
import '../../core/data/repositories/comment_repository.dart';
|
import '../../core/data/repositories/comment_repository.dart';
|
||||||
|
import '../../core/data/services/catalog_controller.dart';
|
||||||
import '../../core/data/services/favorite_controller.dart';
|
import '../../core/data/services/favorite_controller.dart';
|
||||||
import '../../core/data/services/download_controller.dart';
|
import '../../core/data/services/download_controller.dart';
|
||||||
import '../../core/data/services/interaction_controller.dart';
|
import '../../core/data/services/interaction_controller.dart';
|
||||||
import '../../core/data/services/playback_position_cache.dart';
|
import '../../core/data/services/playback_position_cache.dart';
|
||||||
|
import '../../core/data/services/playback_queue.dart';
|
||||||
import '../../core/data/services/playback_session.dart';
|
import '../../core/data/services/playback_session.dart';
|
||||||
import '../../core/data/services/sprite_thumbnail_axis.dart';
|
import '../../core/data/services/sprite_thumbnail_axis.dart';
|
||||||
import '../../core/data/services/playback_thumbnail_service.dart';
|
import '../../core/data/services/playback_thumbnail_service.dart';
|
||||||
@@ -58,12 +60,22 @@ const double _feedRefreshTopGap = 56;
|
|||||||
class PlayerPageHandoff {
|
class PlayerPageHandoff {
|
||||||
PlayerPageHandoff({
|
PlayerPageHandoff({
|
||||||
required this.logic,
|
required this.logic,
|
||||||
required this.initialIndex,
|
required this.sourceQueue,
|
||||||
|
required this.sourceEpisodes,
|
||||||
|
required this.sourceIndex,
|
||||||
|
required this.seriesDrama,
|
||||||
|
required this.seriesEpisodes,
|
||||||
|
required this.episodeIndex,
|
||||||
required this.prepareSource,
|
required this.prepareSource,
|
||||||
});
|
});
|
||||||
|
|
||||||
final PlayerLogic logic;
|
final PlayerLogic logic;
|
||||||
final int initialIndex;
|
final PlaybackQueue sourceQueue;
|
||||||
|
final List<Episode> sourceEpisodes;
|
||||||
|
final int sourceIndex;
|
||||||
|
final Drama seriesDrama;
|
||||||
|
final List<Episode> seriesEpisodes;
|
||||||
|
final int episodeIndex;
|
||||||
final Future<void> Function() prepareSource;
|
final Future<void> Function() prepareSource;
|
||||||
PageController? destinationController;
|
PageController? destinationController;
|
||||||
VoidCallback? activateDestination;
|
VoidCallback? activateDestination;
|
||||||
@@ -150,6 +162,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
bool _presentationSuspended = false;
|
bool _presentationSuspended = false;
|
||||||
bool _presentationReturning = false;
|
bool _presentationReturning = false;
|
||||||
bool _leavingHandoff = false;
|
bool _leavingHandoff = false;
|
||||||
|
bool _preparingFullPlayer = false;
|
||||||
|
PlayerPageHandoff? _activeHandoff;
|
||||||
PageController? _returnPageController;
|
PageController? _returnPageController;
|
||||||
final Set<String> _precachedPosterUrls = {};
|
final Set<String> _precachedPosterUrls = {};
|
||||||
|
|
||||||
@@ -185,10 +199,12 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
);
|
);
|
||||||
_pageController = logic.pages;
|
_pageController = logic.pages;
|
||||||
} else {
|
} else {
|
||||||
final position = logic.visibleIndices.indexOf(handoff.initialIndex);
|
logic.adoptSeriesHandoff(
|
||||||
_pageController = PageController(
|
drama: handoff.seriesDrama,
|
||||||
initialPage: position < 0 ? 0 : position,
|
seriesEpisodes: handoff.seriesEpisodes,
|
||||||
|
episodeIndex: handoff.episodeIndex,
|
||||||
);
|
);
|
||||||
|
_pageController = PageController(initialPage: handoff.episodeIndex);
|
||||||
handoff.destinationController = _pageController;
|
handoff.destinationController = _pageController;
|
||||||
logic.session.setRouteVisible(widget.active);
|
logic.session.setRouteVisible(widget.active);
|
||||||
}
|
}
|
||||||
@@ -1163,13 +1179,56 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _presentFullPlayer(BuildContext context, int index) async {
|
Future<void> _presentFullPlayer(BuildContext context, int index) async {
|
||||||
if (_presentationActive || !_pageController.hasClients) return;
|
if (_presentationActive ||
|
||||||
|
_preparingFullPlayer ||
|
||||||
|
!_pageController.hasClients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_preparingFullPlayer = true;
|
||||||
|
final sourceIndex = logic.session.currentIndex;
|
||||||
|
final sourceEpisode = logic.session.currentEpisode;
|
||||||
|
final sourceQueue = logic.queue;
|
||||||
|
final sourceEpisodes = List<Episode>.of(logic.sessionEpisodes);
|
||||||
|
var seriesDrama = sourceQueue.entryAt(sourceIndex).drama;
|
||||||
|
try {
|
||||||
|
seriesDrama = await context.read<CatalogController>().requireDetail(
|
||||||
|
seriesDrama,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// 详情不可用时仍使用当前剧已知的集列表。
|
||||||
|
}
|
||||||
|
_preparingFullPlayer = false;
|
||||||
|
if (!mounted ||
|
||||||
|
!context.mounted ||
|
||||||
|
logic.session.currentIndex != sourceIndex ||
|
||||||
|
logic.session.currentEpisode.id != sourceEpisode.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final seriesEpisodes = List<Episode>.of(seriesDrama.episodes);
|
||||||
|
var episodeIndex = seriesEpisodes.indexWhere(
|
||||||
|
(episode) => episode.id == sourceEpisode.id,
|
||||||
|
);
|
||||||
|
if (episodeIndex < 0) {
|
||||||
|
episodeIndex = seriesEpisodes.indexWhere(
|
||||||
|
(episode) => episode.number == sourceEpisode.number,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (episodeIndex < 0) {
|
||||||
|
seriesEpisodes.insert(0, sourceEpisode);
|
||||||
|
episodeIndex = 0;
|
||||||
|
}
|
||||||
final sourcePageController = _pageController;
|
final sourcePageController = _pageController;
|
||||||
final handoff = PlayerPageHandoff(
|
final handoff = PlayerPageHandoff(
|
||||||
logic: logic,
|
logic: logic,
|
||||||
initialIndex: index,
|
sourceQueue: sourceQueue,
|
||||||
|
sourceEpisodes: sourceEpisodes,
|
||||||
|
sourceIndex: sourceIndex,
|
||||||
|
seriesDrama: seriesDrama,
|
||||||
|
seriesEpisodes: seriesEpisodes,
|
||||||
|
episodeIndex: episodeIndex,
|
||||||
prepareSource: _prepareSourceAfterFullPlayer,
|
prepareSource: _prepareSourceAfterFullPlayer,
|
||||||
);
|
);
|
||||||
|
_activeHandoff = handoff;
|
||||||
handoff.activateDestination = () {
|
handoff.activateDestination = () {
|
||||||
final destination = handoff.destinationController;
|
final destination = handoff.destinationController;
|
||||||
if (destination != null) logic.pages = destination;
|
if (destination != null) logic.pages = destination;
|
||||||
@@ -1179,8 +1238,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
final route = Navigator.of(context).push<void>(
|
final route = Navigator.of(context).push<void>(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
builder: (_) => PlayerPage(
|
builder: (_) => PlayerPage(
|
||||||
drama: logic.queue.entryAt(index).drama,
|
drama: seriesDrama,
|
||||||
initialIndex: index,
|
initialIndex: episodeIndex,
|
||||||
handoff: handoff,
|
handoff: handoff,
|
||||||
wallet: logic.wallet,
|
wallet: logic.wallet,
|
||||||
downloads: logic.downloads,
|
downloads: logic.downloads,
|
||||||
@@ -1210,17 +1269,23 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
_presentationSuspended = false;
|
_presentationSuspended = false;
|
||||||
_presentationReturning = false;
|
_presentationReturning = false;
|
||||||
});
|
});
|
||||||
|
_activeHandoff = null;
|
||||||
logic.session.setRouteVisible(widget.active);
|
logic.session.setRouteVisible(widget.active);
|
||||||
_logFullPlayerRoute('restored');
|
_logFullPlayerRoute('restored');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _prepareSourceAfterFullPlayer() async {
|
Future<void> _prepareSourceAfterFullPlayer() async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final page = logic.visibleIndices.indexOf(logic.session.currentIndex);
|
final handoff = _activeHandoff;
|
||||||
_returnPageController = PageController(
|
if (handoff == null) return;
|
||||||
initialPage: page < 0 ? 0 : page,
|
|
||||||
);
|
|
||||||
logic.attach(widget, isMounted: () => mounted);
|
logic.attach(widget, isMounted: () => mounted);
|
||||||
|
logic.restoreFeedHandoff(
|
||||||
|
page: widget,
|
||||||
|
sourceQueue: handoff.sourceQueue,
|
||||||
|
sourceEpisodes: handoff.sourceEpisodes,
|
||||||
|
sourceIndex: handoff.sourceIndex,
|
||||||
|
);
|
||||||
|
_returnPageController = PageController(initialPage: handoff.sourceIndex);
|
||||||
setState(() {
|
setState(() {
|
||||||
_presentationSuspended = false;
|
_presentationSuspended = false;
|
||||||
_presentationReturning = true;
|
_presentationReturning = true;
|
||||||
@@ -1247,5 +1312,4 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
'position=${video?.controller.value.position.inMilliseconds ?? '-'}',
|
'position=${video?.controller.value.position.inMilliseconds ?? '-'}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -95,9 +95,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('tab-recommend')));
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final recommendPlayer = find.byKey(
|
final recommendPlayer = find.byKey(const Key('recommend-feed-player'));
|
||||||
const Key('recommend-feed-player'),
|
|
||||||
);
|
|
||||||
final renderStack = find.descendant(
|
final renderStack = find.descendant(
|
||||||
of: recommendPlayer,
|
of: recommendPlayer,
|
||||||
matching: find.byType(PlayerRenderStack),
|
matching: find.byType(PlayerRenderStack),
|
||||||
@@ -107,15 +105,12 @@ void main() {
|
|||||||
final playerState = tester.state(recommendPlayer);
|
final playerState = tester.state(recommendPlayer);
|
||||||
final currentSurface = find.descendant(
|
final currentSurface = find.descendant(
|
||||||
of: recommendPlayer,
|
of: recommendPlayer,
|
||||||
matching: find.byKey(
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
||||||
const ValueKey('player-render-slot-current'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
final sourceRender = tester.widget<PlayerRenderStack>(renderStack);
|
final sourceRender = tester.widget<PlayerRenderStack>(renderStack);
|
||||||
final sourceIndex =
|
final sourceIndex = sourceRender.slots[PlayerRenderSlotRole.current]?.index;
|
||||||
sourceRender.slots[PlayerRenderSlotRole.current]?.index;
|
final currentEpisodeId = factory.episodes.first.id;
|
||||||
final controller = factory.created.first.controller;
|
final controller = factory.created.first.controller;
|
||||||
final createdBefore = factory.created.length;
|
|
||||||
final requestsBefore = playback.requests.length;
|
final requestsBefore = playback.requests.length;
|
||||||
expect(controller.value.position, const Duration(seconds: 5));
|
expect(controller.value.position, const Duration(seconds: 5));
|
||||||
expect(
|
expect(
|
||||||
@@ -132,20 +127,24 @@ void main() {
|
|||||||
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
|
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
|
||||||
expect(fullPlayer.homeMode, isFalse);
|
expect(fullPlayer.homeMode, isFalse);
|
||||||
expect(fullPlayer.handoff, isNotNull);
|
expect(fullPlayer.handoff, isNotNull);
|
||||||
expect(fullPlayer.handoff!.initialIndex, sourceIndex);
|
expect(fullPlayer.handoff!.sourceIndex, sourceIndex);
|
||||||
expect(fullPlayer.handoff!.logic.session.currentIndex, sourceIndex);
|
expect(
|
||||||
|
fullPlayer.handoff!.logic.session.currentIndex,
|
||||||
|
fullPlayer.handoff!.episodeIndex,
|
||||||
|
);
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
|
||||||
expect(find.byKey(const Key('recommend-top-nav')), findsNothing);
|
expect(find.byKey(const Key('recommend-top-nav')), findsNothing);
|
||||||
expect(find.byKey(const Key('home-feed-gesture')), findsNothing);
|
expect(find.byKey(const Key('home-feed-gesture')), findsNothing);
|
||||||
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
||||||
expect(factory.created.length, createdBefore);
|
expect(
|
||||||
|
factory.episodes.where((episode) => episode.id == currentEpisodeId),
|
||||||
|
hasLength(1),
|
||||||
|
);
|
||||||
expect(playback.requests.length, requestsBefore);
|
expect(playback.requests.length, requestsBefore);
|
||||||
expect(controller.value.position, const Duration(seconds: 5));
|
expect(controller.value.position, const Duration(seconds: 5));
|
||||||
final fullCurrentSurface = find.descendant(
|
final fullCurrentSurface = find.descendant(
|
||||||
of: find.byType(PlayerPage),
|
of: find.byType(PlayerPage),
|
||||||
matching: find.byKey(
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
||||||
const ValueKey('player-render-slot-current'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
@@ -165,7 +164,10 @@ void main() {
|
|||||||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||||
expect(recommendPlayer, findsOneWidget);
|
expect(recommendPlayer, findsOneWidget);
|
||||||
expect(renderStack, findsOneWidget);
|
expect(renderStack, findsOneWidget);
|
||||||
expect(factory.created.length, createdBefore);
|
expect(
|
||||||
|
factory.episodes.where((episode) => episode.id == currentEpisodeId),
|
||||||
|
hasLength(1),
|
||||||
|
);
|
||||||
expect(playback.requests.length, requestsBefore);
|
expect(playback.requests.length, requestsBefore);
|
||||||
expect(controller.value.position, const Duration(seconds: 5));
|
expect(controller.value.position, const Duration(seconds: 5));
|
||||||
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
||||||
@@ -195,6 +197,10 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
|
||||||
|
final handoff = fullPlayer.handoff!;
|
||||||
|
final dramaId = handoff.seriesDrama.id;
|
||||||
|
final firstEpisodeId = handoff.logic.session.currentEpisode.id;
|
||||||
final createdBefore = factory.created.length;
|
final createdBefore = factory.created.length;
|
||||||
await tester.drag(
|
await tester.drag(
|
||||||
find.byKey(const Key('player-video-tap')),
|
find.byKey(const Key('player-video-tap')),
|
||||||
@@ -205,8 +211,44 @@ void main() {
|
|||||||
expect(find.byType(PlayerPage), findsOneWidget);
|
expect(find.byType(PlayerPage), findsOneWidget);
|
||||||
expect(factory.created.length, lessThanOrEqualTo(createdBefore + 1));
|
expect(factory.created.length, lessThanOrEqualTo(createdBefore + 1));
|
||||||
expect(find.byKey(const Key('player-render-stack')), findsOneWidget);
|
expect(find.byKey(const Key('player-render-stack')), findsOneWidget);
|
||||||
|
final switchedEpisode = handoff.logic.session.currentEpisode;
|
||||||
|
expect(switchedEpisode.id, isNot(firstEpisodeId));
|
||||||
|
expect(switchedEpisode.number, 2);
|
||||||
|
expect(
|
||||||
|
handoff.logic.queue.entryAt(handoff.logic.session.currentIndex).drama.id,
|
||||||
|
dramaId,
|
||||||
|
);
|
||||||
|
expect(handoff.logic.session.userWantsPlay, isTrue);
|
||||||
|
expect(
|
||||||
|
factory.episodes.where((episode) => episode.id == switchedEpisode.id),
|
||||||
|
hasLength(1),
|
||||||
|
);
|
||||||
|
final switchedVideo = handoff.logic.session.active! as VideoPlayerPort;
|
||||||
|
final switchedController = switchedVideo.controller;
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('player-back')));
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
expect(handoff.logic.session.currentIndex, handoff.sourceIndex);
|
||||||
|
expect(
|
||||||
|
handoff.logic.queue.entryAt(handoff.sourceIndex).episode.id,
|
||||||
|
switchedEpisode.id,
|
||||||
|
);
|
||||||
|
final sourceCurrentSurface = find.descendant(
|
||||||
|
of: find.byType(PlayerPage),
|
||||||
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.widget<PlayerVideoSurface>(sourceCurrentSurface).controller,
|
||||||
|
same(switchedController),
|
||||||
|
);
|
||||||
|
expect(switchedController.value.position, const Duration(seconds: 5));
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: sourceCurrentSurface,
|
||||||
|
matching: find.byKey(const Key('player-single-placeholder')),
|
||||||
|
),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_show_one_paused_overlay_when_entering_full_player', (
|
testWidgets('should_show_one_paused_overlay_when_entering_full_player', (
|
||||||
@@ -236,6 +278,48 @@ void main() {
|
|||||||
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_keep_drama_and_episode_indices_separate', (tester) async {
|
||||||
|
await _pumpApp(tester);
|
||||||
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final feedPage = find
|
||||||
|
.descendant(
|
||||||
|
of: find.byKey(const Key('recommend-feed-player')),
|
||||||
|
matching: find.byType(PageView),
|
||||||
|
)
|
||||||
|
.first;
|
||||||
|
await tester.drag(feedPage, const Offset(0, -620));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final sourceRender = tester.widget<PlayerRenderStack>(
|
||||||
|
find.byType(PlayerRenderStack),
|
||||||
|
);
|
||||||
|
final sourceIndex = sourceRender.slots[PlayerRenderSlotRole.current]?.index;
|
||||||
|
expect(sourceIndex, isNotNull);
|
||||||
|
expect(sourceIndex, greaterThan(0));
|
||||||
|
final feedIndex = sourceIndex!;
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
|
||||||
|
final handoff = fullPlayer.handoff!;
|
||||||
|
expect(handoff.sourceIndex, feedIndex);
|
||||||
|
expect(handoff.episodeIndex, 0);
|
||||||
|
expect(handoff.logic.session.currentIndex, handoff.episodeIndex);
|
||||||
|
expect(
|
||||||
|
handoff.seriesDrama.id,
|
||||||
|
handoff.sourceQueue.entryAt(feedIndex).drama.id,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
handoff.logic.session.currentEpisode.id,
|
||||||
|
handoff.sourceEpisodes[feedIndex].id,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('player-back')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_render_player_loading_indicators_at_compact_size', (
|
testWidgets('should_render_player_loading_indicators_at_compact_size', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -940,10 +1024,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
await _pumpApp(
|
await _pumpApp(tester, catalog: FakeCatalogRepository(dramas: [longCopy]));
|
||||||
tester,
|
|
||||||
catalog: FakeCatalogRepository(dramas: [longCopy]),
|
|
||||||
);
|
|
||||||
await _openSeriesPlayer(
|
await _openSeriesPlayer(
|
||||||
tester,
|
tester,
|
||||||
cardKey: const Key('home-feed-card-long-copy-demo'),
|
cardKey: const Key('home-feed-card-long-copy-demo'),
|
||||||
@@ -966,7 +1047,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(tester.widget<Text>(synopsis).maxLines, 2);
|
expect(tester.widget<Text>(synopsis).maxLines, 2);
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Padding>(find.byKey(const Key('player-synopsis-gap'))).padding,
|
tester
|
||||||
|
.widget<Padding>(find.byKey(const Key('player-synopsis-gap')))
|
||||||
|
.padding,
|
||||||
const EdgeInsets.only(left: 6),
|
const EdgeInsets.only(left: 6),
|
||||||
);
|
);
|
||||||
final synopsisRect = tester.getRect(synopsis);
|
final synopsisRect = tester.getRect(synopsis);
|
||||||
@@ -983,10 +1066,15 @@ void main() {
|
|||||||
find.descendant(of: toggle, matching: find.text('Less')),
|
find.descendant(of: toggle, matching: find.text('Less')),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(tester.widget<Text>(find.byKey(const Key('player-synopsis'))).maxLines, isNull);
|
expect(
|
||||||
|
tester.widget<Text>(find.byKey(const Key('player-synopsis'))).maxLines,
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
final lessRect = tester.getRect(toggle);
|
final lessRect = tester.getRect(toggle);
|
||||||
final textRect = tester.getRect(find.byKey(const Key('player-synopsis')));
|
final textRect = tester.getRect(find.byKey(const Key('player-synopsis')));
|
||||||
final block = tester.getRect(find.byKey(const Key('player-episode-synopsis')));
|
final block = tester.getRect(
|
||||||
|
find.byKey(const Key('player-episode-synopsis')),
|
||||||
|
);
|
||||||
expect(lessRect.top, greaterThanOrEqualTo(textRect.bottom - 1));
|
expect(lessRect.top, greaterThanOrEqualTo(textRect.bottom - 1));
|
||||||
expect(block.right - lessRect.right, lessThanOrEqualTo(2));
|
expect(block.right - lessRect.right, lessThanOrEqualTo(2));
|
||||||
|
|
||||||
@@ -1596,10 +1684,12 @@ Future<void> _pumpApp(
|
|||||||
|
|
||||||
class _RouteVideoFactory {
|
class _RouteVideoFactory {
|
||||||
final List<_RouteVideoPlayer> created = [];
|
final List<_RouteVideoPlayer> created = [];
|
||||||
|
final List<Episode> episodes = [];
|
||||||
|
|
||||||
PlayerPort create(Episode episode) {
|
PlayerPort create(Episode episode) {
|
||||||
final player = _RouteVideoPlayer(episode);
|
final player = _RouteVideoPlayer(episode);
|
||||||
created.add(player);
|
created.add(player);
|
||||||
|
episodes.add(episode);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user