初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
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,
),
),
);
}
}