feat(home): 首页按内容类型分频道 Feed 并支持分页

This commit is contained in:
Zhou Weidong committed 2026-10-21 17:33:52 +08:00
1 parent 4d0be47ee3
commit ae61d9ec4c
22 files changed
+1001 -71

No files matched your search

+87 -5
View File
@@ -100,6 +100,8 @@ class PlayerLogic extends ChangeNotifier {
ValueChanged<Drama>? onDramaChanged;
ValueChanged<bool>? onImmersiveChanged;
bool _disposed = false;
PlayerFactory? _sessionFactory;
PlaybackCoordinator? _sessionCoordinator;
/// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化,
/// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。
@@ -109,6 +111,14 @@ class PlayerLogic extends ChangeNotifier {
bool get chromeHidden => immersive;
/// 当前 PageView 对应用户正在观看的短剧 id,尚未起播时为 null。
String? get currentDramaId {
if (!_started || visibleIndices.isEmpty) return null;
final index = session.currentIndex;
if (index < 0 || index >= queue.length) return null;
return queue.entryAt(index).drama.id;
}
Episode displayEpisode(Drama drama, Episode fallback) {
final number = pickedEpisode[drama.id];
if (number == null) return fallback;
@@ -145,6 +155,8 @@ class PlayerLogic extends ChangeNotifier {
required PlaybackCoordinator coordinator,
}) {
_started = true;
_sessionFactory = factory;
_sessionCoordinator = coordinator;
pictureInPicture.setStateListener(onPipChanged);
queue = feedDramas == null
? PlaybackQueue.series(pageDrama)
@@ -153,7 +165,53 @@ class PlayerLogic extends ChangeNotifier {
pages = PageController(initialPage: initialIndex);
// 票据解析后要把新地址写回这个列表(PlaybackSession 的 canPlay 读的就是它)
sessionEpisodes = [for (final entry in queue.entries) entry.episode];
session = PlaybackSession(
session = _createSession(factory, coordinator);
if (!active || visibleIndices.isEmpty) {
session.setRouteVisible(false);
}
unawaited(loadResumePoint());
unawaited(selectEpisode(initialIndex));
}
/// 原地替换首页 feed 队列;频道变化时重置到第 1 个,刷新时尽量保留当前剧。
void updateFeed(List<Drama> dramas, {String? keepDramaId}) {
final currentId = currentDramaId;
final nextQueue = PlaybackQueue.home(List<Drama>.unmodifiable(dramas));
final nextVisible = List<int>.generate(nextQueue.length, (index) => index);
final nextEpisodes = [for (final entry in nextQueue.entries) entry.episode];
feedDramas = List<Drama>.unmodifiable(dramas);
queue = nextQueue;
visibleIndices = nextVisible;
if (dramas.isNotEmpty) pageDrama = dramas.first;
if (!_started) return;
if (nextQueue.length == 0) {
session.setRouteVisible(false);
notify();
return;
}
final keepIndex = keepDramaId != null && keepDramaId == currentId
? nextQueue.entries.indexWhere((entry) => entry.drama.id == keepDramaId)
: -1;
if (keepIndex >= 0 && session.active != null) {
sessionEpisodes
..clear()
..addAll(nextEpisodes);
session.remapCurrentIndex(
keepIndex,
preloadIndex: nextVisibleIndex(keepIndex),
);
_jumpToFeedPage(keepIndex);
notify();
return;
}
_replaceSession(nextEpisodes, keepIndex >= 0 ? keepIndex : 0);
}
PlaybackSession _createSession(
PlayerFactory factory,
PlaybackCoordinator coordinator,
) {
return PlaybackSession(
episodes: sessionEpisodes,
factory: factory,
// 地址只由后端下发(票据或详情里的免费集签名地址),拿不到地址即视为锁定
@@ -162,11 +220,35 @@ class PlayerLogic extends ChangeNotifier {
activate: activatePlayer,
coordinator: coordinator,
);
if (!active || visibleIndices.isEmpty) {
session.setRouteVisible(false);
}
void _replaceSession(List<Episode> nextEpisodes, int targetIndex) {
final factory = _sessionFactory;
final coordinator = _sessionCoordinator;
if (factory == null || coordinator == null) return;
unawaited(saveCurrentProgress());
final previous = session;
sessionEpisodes = List<Episode>.of(nextEpisodes);
trackedPlayer = null;
trackedEpisode = null;
trackedDrama = null;
session = _createSession(factory, coordinator);
session.setRouteVisible(active && visibleIndices.isNotEmpty);
unawaited(previous.close());
_jumpToFeedPage(targetIndex);
unawaited(selectEpisode(targetIndex));
notify();
}
void _jumpToFeedPage(int index) {
if (!_started) return;
if (pages.hasClients) {
pages.jumpToPage(index);
return;
}
unawaited(loadResumePoint());
unawaited(selectEpisode(initialIndex));
WidgetsBinding.instance.addPostFrameCallback((_) {
if (pages.hasClients) pages.jumpToPage(index);
});
}
/// 服务端续看位置:进播放页时取一次,用于"定位到上次那一集的那一秒"。