195 lines
6.7 KiB
Dart
195 lines
6.7 KiB
Dart
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,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|