This commit is contained in:
lz_db
2026-10-25 16:14:26 +08:00
parent 7fae7387f5
commit 1d9cd1f9c7
10 changed files with 288 additions and 13 deletions
+50
View File
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/log/action_logger.dart';
import '../../core/storage/feed_progress_store.dart';
import '../../core/theme/app_theme.dart';
import '../../models/video_item.dart';
import '../../providers/feed_provider.dart';
@@ -30,10 +31,25 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
final PageController _pageController = PageController();
StreamSubscription<FeedEvent>? _eventSub;
/// 已完成首屏位置同步的策略(冷启动续播跳页只需一次)
final Set<FeedStrategy> _initialSyncDone = {};
/// 定时兜底保存播放进度(被杀进程时也能续播)
Timer? _progressTimer;
/// App 是否在前台(退后台后不写进度)
bool _appActive = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// 冷启动恢复上次所在的 Tab(口碑/热播等),再按该策略的进度续播
final lastName = FeedProgressStore.I.readLastStrategy();
_strategy = FeedStrategy.values.firstWhere(
(s) => s.name == lastName && s != FeedStrategy.search,
orElse: () => FeedStrategy.recommend,
);
// 登记当前策略,HomeShell 切回首页时只恢复这个策略的播放器池
ref.read(activeFeedStrategyProvider.notifier).state = _strategy;
// 监听计费/UI 事件(弹窗与 Toast 由 Notifier 事件流驱动)
@@ -46,6 +62,11 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) ref.read(feedProvider(_strategy).notifier).rebindCurrent();
});
_progressTimer = Timer.periodic(const Duration(seconds: 5), (_) {
if (!mounted || !_appActive) return;
if (!ref.read(feedTabVisibleProvider)) return;
unawaited(ref.read(feedProvider(_strategy).notifier).persistProgress());
});
}
@override
@@ -78,6 +99,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
void dispose() {
WidgetsBinding.instance.removeObserver(this);
routeObserver.unsubscribe(this);
_progressTimer?.cancel();
_eventSub?.cancel();
// 离开 Feed 路由(如跳登录页):彻底停止播放(断音+断缓存下载)。
// feedProvider 是全局常驻,不处理则播放器池在后台继续出声
@@ -93,10 +115,15 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
void didChangeAppLifecycleState(AppLifecycleState state) {
final notifier = ref.read(feedProvider(_strategy).notifier);
if (state == AppLifecycleState.paused) {
_appActive = false;
// 先存进度再 suspend(suspend 会 stop 槽位,位置随即清零)
unawaited(notifier.persistProgress());
notifier.pool.suspend();
} else if (state == AppLifecycleState.inactive) {
unawaited(notifier.persistProgress());
notifier.pool.pauseCurrent();
} else if (state == AppLifecycleState.resumed) {
_appActive = true;
// 仅在首页 Tab 可见时恢复(在"我的"等页面回前台不播)
if (ref.read(feedTabVisibleProvider)) {
unawaited(() async {
@@ -121,9 +148,16 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
void _switchStrategy(FeedStrategy next) {
if (next == _strategy) return;
// 先保存旧策略进度再挂起(suspend 会 stop 槽位,位置随即清零)
unawaited(ref.read(feedProvider(_strategy).notifier).persistProgress());
// 挂起旧策略池(防在途 open 完成后自动出声)
ref.read(feedProvider(_strategy).notifier).pool.suspend();
unawaited(FeedProgressStore.I.saveLastStrategy(next.name));
setState(() => _strategy = next);
// 会话内已有数据的策略由下方跳页逻辑处理,不再走冷启动位置同步
if (ref.read(feedProvider(next)).items.isNotEmpty) {
_initialSyncDone.add(next);
}
ref.read(activeFeedStrategyProvider.notifier).state = next;
_eventSub?.cancel();
_eventSub = ref
@@ -175,6 +209,22 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
final state = ref.watch(feedProvider(_strategy));
final notifier = ref.read(feedProvider(_strategy).notifier);
// 冷启动续播:列表加载完成后把 PageView 定位到恢复的下标(每个策略一次)。
// Notifier 会在 refresh 时按上次 videoId 恢复 currentIndex,此处只负责跳页,
// 跳页触发的 onPageChanged 会把新列表与播放器池对齐
ref.listen<FeedState>(feedProvider(_strategy), (previous, next) {
if (next.items.isEmpty) return;
if (!_initialSyncDone.add(_strategy)) return;
final target = next.currentIndex;
if (target <= 0) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_pageController.hasClients) return;
if ((_pageController.page?.round() ?? 0) != target) {
_pageController.jumpToPage(target);
}
});
});
return Scaffold(
backgroundColor: Colors.black,
body: Stack(