Files
2026-09-15 15:44:13 +07:00

195 lines
6.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_model/drama_media_info.dart';
import 'package:hgdj/hj_page/drama/drama_detail_logic.dart';
import 'package:hgdj/hj_page/drama/view/drama_bottom_bar.dart';
import 'package:hgdj/hj_page/drama/view/drama_video_player.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
/// 短剧二级页:竖划切集 + 底部剧集条(选集/自动续播)
class DramaDetailPage extends StatefulWidget {
final DramaMediaInfo? drama;
/// 指定进来先播第几集(下标);<0 表示不指定,交给续播位置决定
final int initialIndex;
/// 一级页刷剧模式带进来的秒数:只作用在 [initialIndex] 那一集。
/// 从历史记录/收藏进来的不传,那边的位置由本地续播记录决定
final int initialSeconds;
/// 指定先播哪一集(分集 id),优先级高于 [initialIndex] 和续播位置。缓存页点某一集进来用
final String? initialContentId;
const DramaDetailPage({
super.key,
this.drama,
this.initialIndex = -1,
this.initialSeconds = 0,
this.initialContentId,
});
@override
State<DramaDetailPage> createState() => _DramaDetailPageState();
}
class _DramaDetailPageState extends State<DramaDetailPage> with UniqueTagMixin {
//底部剧集条整体高度(36 内容 + 上下 6 外边距),播放器进度条据此让位
static const _barHeight = 48.0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.primaryColor,
body: GetBuilder<DramaDetailLogic>(
tag: uniqueTag, // 可重复 push,按实例隔离
init: DramaDetailLogic(
widget.drama,
initialIndex: widget.initialIndex,
initialContentId: widget.initialContentId,
initialSeconds: widget.initialSeconds,
),
builder: (logic) {
if (!logic.isFirstLoaded) return const LoadingCenterWidget();
if (logic.episodeVideos.isEmpty)
return CErrorWidget(retryOnTap: logic.loadDetail);
return Stack(
fit: StackFit.expand,
children: [
_episodeFeed(),
_backButton(),
_speedButton(logic),
Positioned(left: 0, right: 0, bottom: 0, child: _bottomBar()),
],
);
},
),
);
}
/// 竖向翻集:当前页变化时只 update([idFeed]),各 item 重算 isCurrentPage
Widget _episodeFeed() {
return GetBuilder<DramaDetailLogic>(
tag: uniqueTag,
id: DramaDetailLogic.idFeed,
builder: (logic) => NotificationListener<ScrollNotification>(
onNotification: (n) {
logic.onScroll(n);
return false;
},
child: PageView.builder(
controller: logic.pageCtr,
onPageChanged: logic.onPageChanged,
physics: const ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
allowImplicitScrolling: true,
itemCount: logic.episodeVideos.length,
itemBuilder: (context, index) {
//每集的 VideoModel.id 都是剧 idkey 必须用子集 id,否则 PageView 会跨集复用 State
return DramaVideoPlayer(
key: ValueKey(logic.episodes[index].id ?? index),
index: index,
isCurrentPage: logic.pageIndex == index,
videoInfo: logic.episodeVideos[index],
showEntry: false, // 已经在剧里了,剧名不带箭头、底部条由本页画
isScrolling: logic.isScrolling,
progressBottom: _barHeight + Get.mediaQuery.padding.bottom,
speed: logic.speed,
//带进来的秒数只给落地那一集,其余从本地续播位置起播
startSeconds: logic.takeStartSeconds(index),
onCompleted: logic.onCompleted,
onRemaining: logic.onRemaining,
onCardUnlocked: logic.onCardUnlocked,
);
},
),
),
);
}
/// 底部剧集条:正常展示「观看完整剧集·全N集」,距本集结束 5 秒内切成「Ns后播放下一集」
Widget _bottomBar() {
return SafeArea(
top: false,
child: GetBuilder<DramaDetailLogic>(
tag: uniqueTag,
id: DramaDetailLogic.idBar,
builder: (logic) {
final seconds = logic.nextSeconds;
return DramaBottomBar(
highlight: seconds > 0 ? '${seconds}s后' : null,
text: seconds > 0
? '播放下一集'
: '观看完整剧集·全${logic.info?.totalEpisode ?? logic.episodes.length}集',
onTap: logic.openEpisodeSheet,
);
},
),
);
}
/// 右上角倍速入口(设计稿 NavBar:秒表 18 + 间距 4 + 12/中等,无底无框)
Widget _speedButton(DramaDetailLogic logic) {
return Positioned(
top: 20,
right: 16,
child: SafeArea(
//高度对齐左上角返回键(24 图标 + IconButton 的 8 内边距 = 40),两边看着在一条线上
child: SizedBox(
height: 40,
child: Center(
child: InkWell(
enableFeedback: false,
onTap: logic.openSpeedSheet,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
//导出的图标描边是 #292929,设计稿里是白的,运行时染白
SvgPicture.asset(
'stopwatch.svg'.videoPath,
width: 18,
height: 18,
colorFilter:
const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
4.sizeBoxW,
Text(
'${logic.speed}x',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
height: 18 / 12,
fontWeight: FontWeight.w500),
),
],
),
),
),
),
),
);
}
Widget _backButton() {
return Positioned(
top: 20,
child: SafeArea(
child: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
shadows: [
Shadow(color: Colors.black, offset: Offset(1, 1), blurRadius: 1)
],
),
onPressed: Get.back,
),
),
);
}
}