feat(player): 接入双端画中画播放

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-28 16:36:55 +08:00
1 parent 5346be40a9
commit 43e60e29ec
10 files changed
+1015 -29

No files matched your search

+62
View File
@@ -200,6 +200,8 @@ class _PlayerPageState extends State<PlayerPage>
);
}
_pageController = logic.pages;
// 自动小窗:独立播放页进入即挂;沉浸流随所在 tab 的激活状态挂/摘。
unawaited(logic.syncPipAutoArm(widget.embedded ? widget.active : true));
if (!widget.embedded &&
!logic.isEpisodeLocked(logic.session.currentEpisode)) {
_loadingVisible = true;
@@ -217,6 +219,10 @@ class _PlayerPageState extends State<PlayerPage>
void didUpdateWidget(covariant PlayerPage oldWidget) {
super.didUpdateWidget(oldWidget);
logic.attach(widget, isMounted: () => mounted);
// 沉浸流所在 tab 切换时同步自动小窗(激活挂、切走摘)。
if (widget.embedded && oldWidget.active != widget.active) {
unawaited(logic.syncPipAutoArm(widget.active));
}
if (oldWidget.scrubCancelRequest != widget.scrubCancelRequest) {
_cancelFeedScrub();
}
@@ -703,6 +709,13 @@ class _PlayerPageState extends State<PlayerPage>
onRefresh: _refreshHomeFeed,
child: pages,
);
// 进画中画时预加载当前集海报:还原过渡封面立即可显示,避免再黑一次。
if (logic.pip) {
final poster = logic.trackedEpisode?.posterUrl?.trim();
if (poster != null && poster.isNotEmpty) {
unawaited(precacheImage(NetworkImage(poster), context));
}
}
return Stack(
fit: StackFit.expand,
clipBehavior: Clip.none,
@@ -711,7 +724,13 @@ class _PlayerPageState extends State<PlayerPage>
pageController: _pageController,
slots: _renderSlots(indices),
onCurrentVideoVisible: _handleCurrentVideoVisible,
resumeSignal: logic.pipResumeToken,
),
if (logic.pipRecovering)
Positioned.fill(
key: const Key('player-pip-recover-cover'),
child: _PipRecoverCover(episode: logic.trackedEpisode),
),
if (!widget.embedded && !_loadingRemoved)
Center(
child: IgnorePointer(
@@ -804,6 +823,9 @@ class _PlayerPageState extends State<PlayerPage>
void _handleCurrentVideoVisible(int? index) {
if (!mounted || _loadingRemoved) return;
// 收起还原封面要放在 early-return 之前:恢复时集未变,
// _videoReadyIndex 已相等,只有这里能接到淡入完成的回调。
logic.endPipRecover();
final currentIndex = logic.session.currentIndex;
if (index == null ||
index != currentIndex ||
@@ -1447,3 +1469,43 @@ class _PlayerPageState extends State<PlayerPage>
);
}
}
/// 画中画还原过渡封面:盖住回前台瞬间的黑纹理帧,视频出帧后由
/// [PlayerLogic.endPipRecover] 收起。背景用主题渐变,海报预加载命中前也有过渡观感。
class _PipRecoverCover extends StatelessWidget {
const _PipRecoverCover({required this.episode});
final Episode? episode;
@override
Widget build(BuildContext context) {
final poster = episode?.posterUrl?.trim();
final thumbnail = episode?.thumbnailsUrl?.trim();
final url = poster != null && poster.isNotEmpty
? poster
: (thumbnail != null && thumbnail.isNotEmpty ? thumbnail : null);
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.followingGlow,
AppColors.followingGlowMid,
Color(0xFF101012),
],
stops: [0, 0.22, 0.62],
),
),
child: url == null
? const SizedBox.shrink()
: Image.network(
url,
key: const Key('player-pip-recover-cover-image'),
fit: BoxFit.cover,
gaplessPlayback: true,
errorBuilder: (_, _, _) => const SizedBox.shrink(),
),
);
}
}