初始化
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/drama_media_info.dart';
|
||||
import 'package:hgdj/hj_model/list_base_model.dart';
|
||||
import 'package:hgdj/hj_model/media_content.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:hgdj/hj_page/drama/view/drama_episode_sheet.dart';
|
||||
import 'package:hgdj/hj_page/drama/view/drama_speed_sheet.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
|
||||
/// 短剧二级页:同一部剧连播,退出本页就回到 Feed 的逐剧模式
|
||||
class DramaDetailLogic extends GetxController {
|
||||
// GetBuilder 局部刷新 id:翻页只刷信息流,倒计时只刷底部条
|
||||
static const idFeed = 'feed';
|
||||
static const idBar = 'bar';
|
||||
|
||||
// ===== 外部传入 =====
|
||||
final DramaMediaInfo? drama; // 列表带过来的剧信息(先渲染,详情回来再覆盖)
|
||||
|
||||
/// 指定先播第几集(下标);<0 表示不指定,用本地续播位置
|
||||
final int initialIndex;
|
||||
|
||||
/// 指定先播哪一集(分集 id)。优先级高于上面两者——
|
||||
/// 缓存页只存了分集 id,且那一集常落在后面的分页里,按下标定位不到
|
||||
final String? initialContentId;
|
||||
|
||||
/// [initialIndex] 那一集的起播秒数,由一级页刷剧模式透传;其余情况为 0
|
||||
int _startSeconds;
|
||||
|
||||
DramaDetailLogic(
|
||||
this.drama, {
|
||||
this.initialIndex = -1,
|
||||
this.initialContentId,
|
||||
int initialSeconds = 0,
|
||||
}) : _startSeconds = initialSeconds;
|
||||
|
||||
// ===== 状态 =====
|
||||
bool isFirstLoaded = false;
|
||||
bool _isLoading = false; // 分集分页加载中,防并发
|
||||
|
||||
// ===== 数据 =====
|
||||
DramaMediaInfo? _detail;
|
||||
|
||||
//播放器要的 VideoModel:只在拉到分集时转一次并缓存。
|
||||
//每次 build 现转会换新对象,播放器 didUpdateWidget 里跟着换 model,详情(权限)就会被反复重拉
|
||||
List<VideoModel> episodeVideos = [];
|
||||
|
||||
//剧信息以详情为准,详情没回来先用列表带过来的
|
||||
DramaMediaInfo? get info => _detail ?? drama;
|
||||
|
||||
//分集列表挂在剧信息上
|
||||
List<MediaContent> get episodes => info?.episodeList ?? [];
|
||||
|
||||
bool get isLast => pageIndex >= episodes.length - 1;
|
||||
|
||||
// ===== 交互态 =====
|
||||
int pageIndex = 0;
|
||||
int nextSeconds = 0; // >0 时底部条显示「Ns后播放下一集」
|
||||
double speed = 1.0; // 播放倍速,整页共用:切集沿用,退页即回默认
|
||||
|
||||
// ===== Controller =====
|
||||
late PageController pageCtr;
|
||||
final ValueNotifier<bool> isScrolling = ValueNotifier(false); // 上下滑动切集时隐藏进度条
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
pageIndex = initialIndex < 0 ? 0 : initialIndex;
|
||||
pageCtr = PageController(initialPage: pageIndex);
|
||||
loadDetail();
|
||||
}
|
||||
|
||||
Future<void> loadDetail() async {
|
||||
isFirstLoaded = false;
|
||||
update();
|
||||
try {
|
||||
//三件事互不依赖:续播位置读本地盘,详情和分集都只认构造带进来的剧 id,
|
||||
//一起发出去,首屏从「读盘→详情→分集」三次串行缩成一次并发。
|
||||
//两个接口必须走 Future.wait 一起等:分开 await 的话前一个抛异常后,
|
||||
//后一个没人接的 Future 就成了 unhandled async error(签名失败时两个会一起抛)
|
||||
final resumeTask = dramaResume.load(); // 读本地盘,内部已兜住异常
|
||||
final resp = await Future.wait([
|
||||
DramaService.fetchDetail(drama?.id),
|
||||
DramaService.fetchEpisodeList(
|
||||
drama?.id ?? '', 1, drama?.episodePageSize ?? 30),
|
||||
]);
|
||||
await resumeTask;
|
||||
_detail = resp[0] as DramaMediaInfo? ?? drama;
|
||||
//分集要等详情落定再挂:info 会从 drama 换成 _detail,挂早了就写进被丢弃的那个对象里
|
||||
_applyEpisodes(resp[1] as ListBaseModel<MediaContent>?);
|
||||
if (initialContentId?.isNotEmpty == true) {
|
||||
//外部点的就是这一集(如缓存页点某一集),续播位置和下标都让路
|
||||
_locateTo(await _indexOfContent(initialContentId));
|
||||
} else if (initialIndex < 0) {
|
||||
_locateTo(await _indexOfContent(dramaResume.of(info?.id)?.contentId));
|
||||
} else {
|
||||
//外部指定了集数(如 Feed 里第一集播完 → 进来接着播下一集),别被续播位置顶走
|
||||
_locateTo(initialIndex);
|
||||
}
|
||||
_touchHistory();
|
||||
} catch (e) {
|
||||
debugLog('短剧详情获取失败:$e');
|
||||
}
|
||||
if (isClosed) return; // 请求期间可能已退页,别对已释放的 controller 刷新
|
||||
isFirstLoaded = true;
|
||||
update();
|
||||
}
|
||||
|
||||
/// 进来就算看过一次:观看时间刷成现在,历史列表里排到最前,进度不动。
|
||||
/// 不能只靠播放器落进度——秒退、或者进来就撞付费墙时它一条都不记(见 _saveResume 的三道闸)
|
||||
void _touchHistory() {
|
||||
final list = episodes;
|
||||
dramaResume.markWatched(
|
||||
mediaId: info?.id,
|
||||
contentId:
|
||||
list.isEmpty ? null : list[pageIndex.clamp(0, list.length - 1)].id,
|
||||
drama: info,
|
||||
);
|
||||
}
|
||||
|
||||
/// 把并发拉回来的第一页分集挂到最终的剧对象上
|
||||
void _applyEpisodes(ListBaseModel<MediaContent>? res) {
|
||||
final media = info;
|
||||
if (media == null) return;
|
||||
media.episodeList = res?.list;
|
||||
media.episodeCurPage = 1;
|
||||
media.haxNextEpisode = res?.hasNext ?? false;
|
||||
episodeVideos = (media.episodeList ?? []).map(media.toVideoModel).toList();
|
||||
}
|
||||
|
||||
/// 开短剧卡之后:整部剧一起放行。只重拉一次剧详情拿最新权限,分集不逐页重拉——
|
||||
/// 付费角标就地清掉,具体哪一集能不能播,等翻到它时 _loadEpisode 拉分集详情为准
|
||||
Future<void> onCardUnlocked() async {
|
||||
final detail = await DramaService.fetchDetail(drama?.id);
|
||||
if (isClosed) return;
|
||||
final media = info;
|
||||
if (media == null) return;
|
||||
if (detail != null) {
|
||||
//分集那几个字段是本页自己维护的,别被新详情清空
|
||||
detail.episodeList = media.episodeList;
|
||||
detail.episodeCurPage = media.episodeCurPage;
|
||||
detail.haxNextEpisode = media.haxNextEpisode;
|
||||
_detail = detail;
|
||||
}
|
||||
//置空而不是改成 bought:开了卡就是不该再挂付费标,真实状态以分集详情为准
|
||||
for (final episode in info?.episodeList ?? <MediaContent>[]) {
|
||||
if (episode.accessType == 'coin' || episode.accessType == 'card')
|
||||
episode.accessType = null;
|
||||
}
|
||||
update([idFeed]);
|
||||
}
|
||||
|
||||
/// 快翻到列表尾部时续拉下一页,别让连播在分页边界断掉。
|
||||
/// 原地 addAll 而不是新建列表:选集面板持有的是同一个 List 引用,换了对象它就永远看不到新数据。
|
||||
/// 只追加新的,也不整表重建 episodeVideos——重建会把在播的那几集换成新对象,播放器跟着重拉一遍详情
|
||||
Future<void> _loadMore() async {
|
||||
final media = info;
|
||||
if (media == null || _isLoading || media.haxNextEpisode != true) return;
|
||||
_isLoading = true;
|
||||
final page = (media.episodeCurPage ?? 1) + 1;
|
||||
final res = await DramaService.fetchEpisodeList(
|
||||
media.id ?? '', page, media.episodePageSize);
|
||||
_isLoading = false;
|
||||
if (isClosed) return;
|
||||
//拉空/失败都要落 haxNextEpisode,否则 _resumeIndex 的补页循环会一直转
|
||||
if (res?.list?.isNotEmpty != true) {
|
||||
media.haxNextEpisode = false;
|
||||
return;
|
||||
}
|
||||
final more = res!.list!;
|
||||
(media.episodeList ??= []).addAll(more);
|
||||
media.episodeCurPage = page;
|
||||
media.haxNextEpisode = res.hasNext ?? false;
|
||||
episodeVideos.addAll(more.map(media.toVideoModel));
|
||||
update([idFeed]);
|
||||
}
|
||||
|
||||
/// 某一集在列表里的下标。目标集常落在后面的分页里,这里边翻页边找——
|
||||
/// 分集按集数有序,找到那一页就停,拉的页数和「先算出第几集再拉够长度」一样多,
|
||||
/// 却不依赖「第 N 集就在第 N 个位置」(集数跳号就会算少)。没传/找不到(已下架)都回 0
|
||||
Future<int> _indexOfContent(String? contentId) async {
|
||||
if (contentId == null) return 0;
|
||||
var index = episodes.indexWhere((e) => e.id == contentId);
|
||||
while (index < 0 && !isClosed && info?.haxNextEpisode == true) {
|
||||
final before = episodes.length;
|
||||
await _loadMore();
|
||||
if (episodes.length == before) break; // 没拉到新数据,别死转
|
||||
index = episodes.indexWhere((e) => e.id == contentId);
|
||||
}
|
||||
return index < 0 ? 0 : index;
|
||||
}
|
||||
|
||||
/// 定位到某一集(具体秒数由播放器按本地记录续)。
|
||||
/// 下标必须钳在已加载范围内:只有 1 集的剧收到「播下一集」时会越界,PageView 直接崩。
|
||||
/// isClosed 也要挡:补分页那几个请求还在飞时用户退了页,pageCtr 已在 onClose 里释放,
|
||||
/// 走到下面的 dispose 就是二次释放
|
||||
void _locateTo(int index) {
|
||||
if (isClosed || episodes.isEmpty) return;
|
||||
final target = index.clamp(0, episodes.length - 1);
|
||||
if (target == pageIndex) return;
|
||||
pageIndex = target;
|
||||
//错误页重试进来时 PageView 可能还挂着旧 controller,那种情况只能跳页,dispose 会崩在 detach 上
|
||||
if (pageCtr.hasClients) {
|
||||
pageCtr.jumpToPage(target);
|
||||
return;
|
||||
}
|
||||
pageCtr.dispose();
|
||||
pageCtr = PageController(initialPage: target);
|
||||
}
|
||||
|
||||
void onPageChanged(int index) {
|
||||
_clearNextTip();
|
||||
if (pageIndex == index) return;
|
||||
pageIndex = index;
|
||||
update([idFeed]);
|
||||
if (episodes.length - index <= 5) _loadMore();
|
||||
}
|
||||
|
||||
/// 播放器报来的「距结束还剩 N 秒」:连播时底部条提前倒数,不是播完再等 5 秒
|
||||
void onRemaining(int seconds) {
|
||||
if (isLast) return; // 最后一集不提示下一集
|
||||
if (nextSeconds == seconds) return;
|
||||
nextSeconds = seconds;
|
||||
update([idBar]);
|
||||
}
|
||||
|
||||
/// 本集播完:连播直接切下一集;最后一集播完就停,不回第一集
|
||||
void onCompleted() {
|
||||
_clearNextTip();
|
||||
if (isLast) return;
|
||||
pageCtr.animateToPage(
|
||||
pageIndex + 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
void _clearNextTip() {
|
||||
if (nextSeconds == 0) return;
|
||||
nextSeconds = 0;
|
||||
update([idBar]);
|
||||
}
|
||||
|
||||
/// 打开倍速面板。倍速挂在页面上不是某一集上,切集后新起的播放器也按它播
|
||||
Future<void> openSpeedSheet() async {
|
||||
final value = await DramaSpeedSheet.show(speed: speed);
|
||||
if (isClosed || value == null || value == speed) return;
|
||||
speed = value;
|
||||
update(); // 右上角按钮文案 + 各集播放器一起刷
|
||||
}
|
||||
|
||||
/// 打开选集面板,选中后跳到对应集
|
||||
Future<void> openEpisodeSheet() async {
|
||||
_clearNextTip();
|
||||
final index = await DramaEpisodeSheet.show(
|
||||
drama: info,
|
||||
episodes: episodes, // 与 logic 共用同一个 List 实例,续拉的新集面板里能直接看到
|
||||
selectedIndex: pageIndex,
|
||||
onLoadMore: _loadMore,
|
||||
);
|
||||
//选集面板是异步的,回来时页面可能已经退掉,pageCtr 已释放
|
||||
if (isClosed || index == null || index == pageIndex) return;
|
||||
pageCtr.jumpToPage(index);
|
||||
}
|
||||
|
||||
/// 取走带进来的起播秒数:只给落地那一集,且只给一次——
|
||||
/// PageView 滑远会把那一集的 State 回收,再滑回来重建时不清零的话会又 seek 回原位,
|
||||
/// 而滑回来的口径是从头重看
|
||||
int takeStartSeconds(int index) {
|
||||
if (index != initialIndex || _startSeconds <= 0) return 0;
|
||||
final seconds = _startSeconds;
|
||||
_startSeconds = 0;
|
||||
return seconds;
|
||||
}
|
||||
|
||||
/// 上下滑动切集时隐藏进度条。只认起止,中间的 update 通知不用管
|
||||
void onScroll(ScrollNotification n) {
|
||||
if (n is ScrollStartNotification) {
|
||||
isScrolling.value = true;
|
||||
} else if (n is ScrollEndNotification) {
|
||||
isScrolling.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// GetX 释放只走 onClose(),定时器/控制器都在这里清
|
||||
@override
|
||||
void onClose() {
|
||||
pageCtr.dispose();
|
||||
isScrolling.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user