初始化
This commit is contained in:
@@ -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 都是剧 id,key 必须用子集 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user