Skip to content

feat(season): 支持根据下一个播放的剧集聚焦季与集#282

Open
Daydreamer-riri wants to merge 13 commits intoGhostenEditor:devfrom
Daydreamer-riri:feature/season-init
Open

feat(season): 支持根据下一个播放的剧集聚焦季与集#282
Daydreamer-riri wants to merge 13 commits intoGhostenEditor:devfrom
Daydreamer-riri:feature/season-init

Conversation

@Daydreamer-riri
Copy link

@Daydreamer-riri Daydreamer-riri commented Dec 28, 2025

Summary

本 PR 新增 FocusListController 统一管理“记忆焦点/恢复焦点”逻辑,并将其应用于季列表与集列表;同时在当前季包含 nextToPlay 时自动滚动到对应集位置。

Motivation

  • 复用焦点管理逻辑,便于后续在其他列表中统一体验
  • 列表间切换时可恢复上次焦点,提升导航效率
  • 进入季详情时可直达 nextToPlay 集,减少手动查找

Key Changes

  • 新增 FocusListController,统一管理焦点节点与最后焦点状态
  • 左侧季列表与右侧集列表接入焦点记忆,面板切换自动恢复焦点
  • 在 nextToPlay 属于当前季时,自动滚动到对应集的位置

Preview

vedio

@Daydreamer-riri Daydreamer-riri changed the title feat(season): 支持根据下一个播放的剧集切换季与集 feat(season): 支持根据下一个播放的剧集聚焦季与集 Dec 29, 2025
Copy link
Owner

@GhostenEditor GhostenEditor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我不太明白为什么要加这个功能,感觉没什么必要
而且此处代码也未达到预期效果

@Daydreamer-riri
Copy link
Author

如果是多季的剧集,我只想在最近看的季或者最近看的集附近操作,但是每次打开列表都是第一季第一集。尤其是有特别篇或者未知季时会聚焦到特别篇。我每一次进入这个页面的第一个操作100%是更换季。我不确定奈飞的默认行为是什么。

因为我没验证,所以不知道有没有效果……

@GhostenEditor
Copy link
Owner

对,应该把季的排序改一下,特别季放最后,我会调整。
我想了一下,你说的有道理,可以加这个功能。

@Daydreamer-riri Daydreamer-riri marked this pull request as draft January 23, 2026 15:54
@Daydreamer-riri Daydreamer-riri marked this pull request as ready for review February 1, 2026 17:20
@Daydreamer-riri Daydreamer-riri marked this pull request as draft February 2, 2026 03:21
@Daydreamer-riri Daydreamer-riri marked this pull request as ready for review February 3, 2026 17:49
@Daydreamer-riri
Copy link
Author

Daydreamer-riri commented Feb 3, 2026

Hi,这个 PR 已经就绪。出了首次进入聚焦之外,为章节页面提供了更符合直觉的焦点管理(抽象出了 FocusScopeManager,后续可用来优化首页的多个列表的焦点)。滚动方面为了避免抖动或者长列表的滚动动画,同时不引入其他依赖,我选择了计算 initialScrollOffset 的形式。如果你有更好的实现,请直接告诉我(我对 flutter 不算熟悉,所以不确定是否有更好的实现),我会参考优化。

@GhostenEditor GhostenEditor changed the base branch from main to releases/v2.4.0 February 8, 2026 09:50
Copy link
Owner

@GhostenEditor GhostenEditor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感觉你像复杂了,你已经在上面计算了ScrollController的offset,功能都基本实现了,没必要有再嵌套两个FocusScope,还要处理跳转。

可以参考下这里的代码,但是获取数据的逻辑得改一下

class PlayerPlaylistView<T> extends StatefulWidget {
const PlayerPlaylistView({super.key, required this.onTap, this.activeIndex, required this.playlist});
final ValueChanged<int> onTap;
final int? activeIndex;
final List<PlaylistItemDisplay<dynamic>> playlist;
@override
State<PlayerPlaylistView<T>> createState() => _PlayerPlaylistViewState<T>();
}
class _PlayerPlaylistViewState<T> extends State<PlayerPlaylistView<T>> {
late final _controller = ScrollController(
initialScrollOffset: min(
(widget.activeIndex ?? 0) * 216,
max(widget.playlist.length * 216 - MediaQuery.of(context).size.width + 64, 0),
),
);
@override
void didUpdateWidget(covariant PlayerPlaylistView<T> oldWidget) {
final index = widget.activeIndex;
if (index != oldWidget.activeIndex && index != null && index >= 0 && index < widget.playlist.length) {
final offset = min(_controller.position.maxScrollExtent, index * (200.0 + 12));
_controller.animateTo(offset, duration: const Duration(milliseconds: 400), curve: Curves.easeOut);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scrollbar(
controller: _controller,
child: ListView.builder(
controller: _controller,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 48),
itemCount: widget.playlist.length,
itemExtent: 216,
itemBuilder: (context, index) {
final item = widget.playlist[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(
width: 200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
Stack(
children: [
FocusableImage(
width: 200,
height: 112,
poster: item.poster,
autofocus: index == widget.activeIndex,
onTap: () => widget.onTap(index),
),
if (index == widget.activeIndex)
Container(
width: 200,
height: 112,
decoration: BoxDecoration(color: Colors.black45, borderRadius: BorderRadius.circular(6)),
child: Align(
alignment: Alignment.topRight,
child: PlayingIcon(color: Theme.of(context).colorScheme.primary),
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.title != null)
Text(
item.title!,
style: Theme.of(context).textTheme.titleSmall,
overflow: TextOverflow.ellipsis,
),
if (item.description != null)
Text(
item.description!,
style: Theme.of(context).textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
],
),
],
),
),
);
},
),
);
}
}

@Daydreamer-riri
Copy link
Author

Daydreamer-riri commented Feb 8, 2026

两个 FocusScoped 是为了记忆焦点位置(Focusscoped 会自动记忆区域内最后一个焦点位置),这个部分其实应该属于另一个 feature,如果你希望可以拆到另一个 pr。后续我想尝试将类似的方法引入首页。

相关 issue #300

@Daydreamer-riri
Copy link
Author

Hi @GhostenEditor,关于 offset 计算的点,我已经使用了固定的值进行计算,移除了 LayoutBuilder。请再审阅一次。

@GhostenEditor
Copy link
Owner

两个 FocusScoped 是为了记忆焦点位置(Focusscoped 会自动记忆区域内最后一个焦点位置),这个部分其实应该属于另一个 feature,如果你希望可以拆到另一个 pr。后续我想尝试将类似的方法引入首页。

是的,这两个FocusScope不应该在这个pr提交,而且我感觉实现方式不太好,请在此pr忽略该问题。然后scroll offest的计算方式,直接用itemHeight*index+headerHeight,具体的计算方式放在注释里。

@GhostenEditor GhostenEditor changed the base branch from releases/v2.4.0 to dev February 13, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants