import 'dart:math'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hgdj/assets_tool/app_colors.dart'; import 'package:hgdj/assets_tool/images.dart'; import 'package:hgdj/extension/extensions.dart'; import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart'; import 'package:hgdj/tools_base/toast.dart'; import 'package:hgdj/tools_base/unique_tag_mixin.dart'; import 'package:hgdj/tools_base/widget/follow_button.dart'; import 'package:hgdj/tools_base/widget/keep_alive_widget.dart'; import 'package:hgdj/tools_base/widget/net_image_widget.dart'; import 'package:provider/provider.dart'; import '../../../hj_utils/const.dart'; import '../../../hj_utils/screen.dart'; import '../../../tools_base/loading/loading_center_widget.dart'; import '../../alert/video/share_media_dialog.dart'; import '../../hj_model/video_model.dart'; import '../../hj_utils/widget_util.dart'; import '../home/tag/cartoon_tag_page.dart'; import 'cartoon_detail_logic.dart'; import 'cartoon_recommend_page.dart'; import 'widget/acg_expand_text.dart'; import 'widget/acg_source_manager.dart'; import 'widget/free_badge.dart'; //acg详情页,小说和漫画 合并 class CartoonDetailPage extends StatefulWidget { final String id; final String? heroTag; const CartoonDetailPage({super.key, this.id = '', this.heroTag}); @override State createState() => _CartoonDetailPageState(); } // 详情页可经「推荐页 → 再开详情页」成环并存,用 UniqueTagMixin 给每个实例唯一 tag class _CartoonDetailPageState extends State with UniqueTagMixin { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Color(0xff05000E), body: GetBuilder( tag: uniqueTag, init: CartoonDetailLogic(id: widget.id), builder: (logic) { // manager 作为 ChangeNotifier 供子树 Consumer 监听; // 用 create: 让 Provider 在页面卸载时自动 dispose manager(释放音频播放器+订阅),Flutter 层保证释放 return ChangeNotifierProvider( create: (_) => logic.manager, child: Builder(builder: (_) { if (logic.mediaInfo == null) return LoadingCenterWidget(); if (logic.mediaInfo?.id == null) { return CErrorWidget(retryOnTap: () => logic.loadData()); } return _buildContent(logic); }), ); }, ), ); } // 当前内容对应的推荐类型(小说等无对应 tab 时为 null) MediaStyle? _selfStyle(CartoonDetailLogic logic) => switch (logic.mediaInfo?.mediaType) { 'video' => MediaStyle.Cartoon, 'image' => MediaStyle.Comics, _ => null, }; // 推荐 tab 排序:与当前内容同类的排最前,其余按默认顺序 List _orderedStyles(CartoonDetailLogic logic) { const base = [MediaStyle.Comics, MediaStyle.Video, MediaStyle.Cartoon]; final self = _selfStyle(logic); if (self == null) return base; return [self, ...base.where((s) => s != self)]; } String _recTitle(MediaStyle s) => switch (s) { MediaStyle.Video => '视频推荐', MediaStyle.Cartoon => '动漫推荐', _ => '漫画推荐', }; // 单个推荐 tab:同类才传 tagId(拉相似),视频 cell 用大图 2 列 Widget _recPage(CartoonDetailLogic logic, MediaStyle s) { final isMatch = s == _selfStyle(logic); final isVideo = s == MediaStyle.Video; return CartoonRecommendPage( mediaStyle: s, mediaId: isMatch ? logic.mediaInfo?.tagDetails?.firstOrNull?.id : null, crossAxisCount: isVideo ? 2 : 3, childAspectRatio: isVideo ? 168 / 142 : 111 / 188, ).keepAlive; } Widget _buildContent(CartoonDetailLogic logic) { final styles = _orderedStyles(logic); return Column( children: [ Expanded( child: Stack( children: [ ExtendedNestedScrollView( controller: logic.scrollCtr, onlyOneScrollInBody: true, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverToBoxAdapter(child: _buildImgHeader(logic)), //header SliverToBoxAdapter(child: _buildMenu(logic)), //目录 SliverToBoxAdapter( child: _buildTabbar(logic, styles)) //推荐 tabbar ]; }, //推荐 body: TabBarView( controller: logic.tabCtr, children: styles.map((s) => _recPage(logic, s)).toList(), ), ), Positioned( top: 0, left: 0, right: 0, child: _buildTopHeader(logic), ), ], )), Padding( // 底部垫上虚拟导航栏高度,避免按钮被遮挡(edge-to-edge) padding: EdgeInsets.only(top: 10, bottom: 10 + screen.paddingBottom), child: Row( children: [ 16.sizeBoxW, if (logic.isCartoonType) ...[ Consumer( builder: (context, manager, child) { bool liked = manager.mediaInfo?.mediaStatus?.hasLiked == true; return InkWell( enableFeedback: false, onTap: () => manager.onLikeAction(), child: Container( width: 108, decoration: BoxDecoration( color: Colors.white.withValues(alpha: .2), borderRadius: BorderRadius.circular(3), ), height: 44, alignment: Alignment.center, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( liked ? 'acg_like_sel.png'.acgImgPath : 'acg_like_nor.png'.acgImgPath, width: 16, ), 10.sizeBoxW, Text( '喜欢', style: textStyle( 16, Color(0xff989898), FontWeight.w500), ), ], )), ); }, ), 12.sizeBoxW, ], Expanded( child: InkWell( enableFeedback: false, onTap: () => logic.play(), child: Container( decoration: BoxDecoration( color: AppColors.actionRed, borderRadius: BorderRadius.circular(3), ), height: 44, alignment: Alignment.center, child: Text( '开始阅读', style: textStyle(16, Colors.white, FontWeight.w500), ), ), ), ), 16.sizeBoxW, ], ), ), ], ); } Widget _buildImgHeader(CartoonDetailLogic logic) { return Stack( children: [ //背景图 Positioned( top: 0, left: 0, right: 0, child: NetworkImageLoader( imageUrl: logic.mediaInfo?.verticalCover ?? "", borderRadius: 0, height: 350, alignment: Alignment.topCenter, ), ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ (10 + screen.paddingTop).sizeBoxH, Row( children: [ _buildBackView(), Spacer(), _buildCollAction(), 12.sizeBoxW, _buildShareView(logic), ], ).paddingSymmetric(horizontal: 16), 154.sizeBoxH, Text( logic.mediaInfo?.title ?? "", style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w900, fontFamily: 'Roboto', ), ).paddingSymmetric(horizontal: 16), 12.sizeBoxH, Container( padding: EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Color(0xff05000E), borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), ), child: Column( children: [ 18.sizeBoxH, Row( children: [ _buildStateItem( 'view_count.png'.communityPath, logic.mediaInfo?.countBrowse?.countStr ?? '', ), 12.sizeBoxW, _buildSpe(), 12.sizeBoxW, // 点赞状态/数量随 manager.notifyListeners 刷新(onLikeAction 不会触发 GetX update) Consumer( builder: (_, manager, __) => _buildStateItem( manager.mediaInfo?.mediaStatus?.hasLiked == true ? 'community_thumb_sel.png'.communityPath : 'like_count.png'.communityPath, manager.mediaInfo?.countLike?.countStr ?? '', ), ), 12.sizeBoxW, _buildSpe(), 12.sizeBoxW, // 0:会员 1:金币购买 2:免费 if (logic.mediaInfo?.permission == 0) ...[ Text( 'VIP', style: textStyle(12, Color(0xff989898), FontWeight.w400), ), ] else if (logic.mediaInfo?.permission == 1) ...[ Text( '${logic.mediaInfo?.price ?? 0}金币', style: textStyle(12, Color(0xff989898), FontWeight.w400), ), ], ], ), //详情 if (logic.mediaInfo?.summary?.isNotEmpty == true) ...[ 12.sizeBoxH, Padding( padding: EdgeInsets.fromLTRB(0, 12, 0, 0), child: AcgShowMoreTextWidget( summary: logic.mediaInfo?.summary ?? '', maxWidth: Get.width - 10, ), ) ], //标签 if ((logic.mediaInfo?.tagDetails?.length ?? 0) != 0) ...[ 12.sizeBoxH, SizedBox( height: 26, child: ListView.separated( separatorBuilder: (context, index) => 6.sizeBoxW, itemCount: logic.mediaInfo?.tagDetails?.length ?? 0, scrollDirection: Axis.horizontal, itemBuilder: (context, index) { TagsBean? tag = logic.mediaInfo?.tagDetails?[index]; return InkWell( enableFeedback: false, onTap: () { Get.to(CartoonTagPage( title: tag?.name ?? '', sId: tag?.id ?? '', )); }, child: Container( padding: EdgeInsets.symmetric(horizontal: 8), alignment: Alignment.center, decoration: BoxDecoration( color: Colors.white.withValues(alpha: .05), borderRadius: BorderRadius.circular(3)), height: 26, child: Text( tag?.name ?? '', style: TextStyle( color: Colors.white.withValues(alpha: .55), fontSize: 12, fontWeight: FontWeight.w400, ), )), ); }, ), ), 12.sizeBoxH, ], ], ), ), ], ), ], ); } Widget _buildSpe() { return Container( color: Colors.white.withValues(alpha: 0.1), height: 18, width: 0.5, ); } Widget _buildStateItem(String img, String name) { return Row( children: [ Image.asset(img, width: 16), 4.sizeBoxW, Text( name, style: textStyle(12, Color(0xff989898), FontWeight.w400), ) ], ); } Widget _buildTabbar(CartoonDetailLogic logic, List styles) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TabBar( controller: logic.tabCtr, padding: EdgeInsets.symmetric(horizontal: 8), tabs: styles .map((s) => Padding( padding: const EdgeInsets.only(bottom: 5.0), child: Text(_recTitle(s)), )) .toList(), tabAlignment: TabAlignment.center, isScrollable: true, labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), labelColor: Colors.white.withValues(alpha: 0.9), unselectedLabelColor: Colors.white.withValues(alpha: .55), unselectedLabelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w400), indicator: CustomIndicator( isGradient: true, width: 13, height: 3, borderRadius: BorderRadius.circular(1.5), )), ], ); } Widget _buildMenu(CartoonDetailLogic logic) { return Container( padding: EdgeInsets.symmetric(horizontal: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ 18.sizeBoxH, Row( children: [ Text( logic.isCartoonType ? '选集' : '共${logic.mediaInfo?.totalEpisode ?? 0}${logic.mediaInfo?.unit}', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), Spacer(), Row( children: [ InkWell( enableFeedback: false, onTap: logic.onMenuAction, child: Text( logic.isCartoonType ? '全${logic.mediaInfo?.totalEpisode ?? 0}${logic.mediaInfo?.unit}' : '目录', style: TextStyle( color: Color(0xFFA7A7A7), fontSize: 14, fontWeight: FontWeight.w400, ), ), ), 2.sizeBoxW, Image.asset( 'arrow_right_grey.webp'.commonImgPath, color: Color(0xff989898), width: 24, ) ], ) ], ), 18.sizeBoxH, Consumer( builder: (context, provider, child) { return ListView.builder( padding: EdgeInsets.zero, physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { final epm = provider.allEpisodes[index]; return InkWell( enableFeedback: false, onTap: () => logic.changeEpisode(index, false), child: Container( padding: EdgeInsets.symmetric( vertical: logic.isCartoonType ? 6 : 12), child: Row( children: [ Flexible( child: Text( '第${epm.episodeNumber ?? ''}${provider.mediaInfo?.unit}${logic.isCartoonType ? '' : ' ${epm.name}'}', maxLines: 1, overflow: TextOverflow.ellipsis, style: textStyle( 16, provider.index == index ? Colors.white.withValues(alpha: .9) : Colors.white.withValues(alpha: .45), provider.index == index ? FontWeight.w500 : FontWeight.w400, ), ), ), //无任何权限时,前 N 集免费展示「免费」角标 if (provider.mediaInfo?.hasPermission == false && epm.inFreeEpisode) ...[ 8.sizeBoxW, const FreeBadge(), ], ], ), ), ); }, itemCount: min(4, provider.allEpisodes.length), ); }, ), 18.sizeBoxH, if (logic.isCartoonType) 0.5.line, 18.sizeBoxH, ], ), ); } Widget _buildBackView() { return InkWell( enableFeedback: false, child: Image.asset( 'back_circle_grey.png'.commonImgPath, width: 24, ), onTap: () => Get.back(), ); } Widget _buildShareView(CartoonDetailLogic logic) { return InkWell( enableFeedback: false, onTap: () { Get.dialog( ShareMediaDialog( videoModel: VideoModel() ..id = logic.mediaInfo?.id ..cover = logic.mediaInfo?.coverH, ), ); }, child: Image.asset("acg_share.png".acgImgPath, width: 24), ); } Widget _buildTopHeader(CartoonDetailLogic logic) { return GetBuilder( tag: uniqueTag, id: 'header', builder: (_) { return Offstage( offstage: logic.showHeader, child: Container( padding: EdgeInsets.only(left: 16, right: 16, top: screen.paddingTop), height: 44 + screen.paddingTop, color: Color(0xff05000E), child: Row( children: [ GestureDetector( onTap: () => Get.back(), child: Icon(Icons.arrow_back_ios, size: 24, color: Colors.white), ), 8.sizeBoxW, Expanded( child: Text( logic.mediaInfo?.title ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, ), ), _buildCollAction(), 12.sizeBoxW, _buildShareView(logic), ], ), ), ); }, ); } Widget _buildCollAction() { return Consumer( builder: (context, manager, child) { bool col = manager.mediaInfo?.mediaStatus?.hasCollected ?? false; return FollowButton( mediaId: manager.mediaInfo?.id ?? '', isFollow: col, followType: FollowEnum.cartoon, successsAction: (isSuccess) { manager.updateCollectState(isSuccess); if (isSuccess) { showToast('收藏成功'); } }, ); }, ); } }