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 createState() => _DramaDetailPageState(); } class _DramaDetailPageState extends State with UniqueTagMixin { //底部剧集条整体高度(36 内容 + 上下 6 外边距),播放器进度条据此让位 static const _barHeight = 48.0; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.primaryColor, body: GetBuilder( 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( tag: uniqueTag, id: DramaDetailLogic.idFeed, builder: (logic) => NotificationListener( 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( 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, ), ), ); } }