import 'dart:math'; 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/hj_utils/screen.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:provider/provider.dart'; import '../../hj_utils/widget_util.dart'; import '../../tools_base/loading/loading_center_widget.dart'; import '../../tools_base/refresh/pull_refresh.dart'; import 'acg_widget_item.dart'; import 'novel_detail_logic.dart'; import 'novel_header.dart'; import 'widget/acg_source_manager.dart'; import 'widget/free_badge.dart'; //小说详情页 - class NovelDetailPage extends StatefulWidget { final String? id; const NovelDetailPage(this.id, {super.key}); @override State createState() => _NovelDetailPageState(); } // 详情页可经「推荐 → 再开详情页」成环并存,用 UniqueTagMixin 给每个实例唯一 tag class _NovelDetailPageState extends State with UniqueTagMixin { @override Widget build(BuildContext context) { return GetBuilder( tag: uniqueTag, init: NovelDetailLogic(widget.id ?? ''), builder: (_) => Scaffold( body: () { if (_.model == null) return LoadingCenterWidget(); if (_.model?.id == null) return CErrorWidget(); // 用 create: 让 Provider 在页面卸载时自动 dispose manager(释放音频播放器+订阅),Flutter 层保证释放 return ChangeNotifierProvider( create: (ctx) => _.manager, child: _buildContent(_), ); }(), ), ); } _buildContent(NovelDetailLogic _) { return Stack( alignment: Alignment.center, children: [ Column( children: [ Expanded( child: pullYsRefresh( onInit: (ctr) => _.refreshCtr = ctr, enablePullDown: false, onLoading: (ctr) => _.fetchRecommendData(), child: CustomScrollView( controller: _.scrollCtr, slivers: [ SliverToBoxAdapter(child: _buildNovelHeader(_)), 18.sliverSizeBoxH, SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text( '相关推荐', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), ), ), ), 12.sliverSizeBoxH, if (_.recommends.isNotEmpty) ...[ SliverPadding( padding: EdgeInsets.symmetric(horizontal: 16), sliver: SliverPadding( padding: EdgeInsets.only(bottom: 80), sliver: SliverGrid.builder( itemCount: _.recommends.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 12, crossAxisSpacing: 6, childAspectRatio: 115 / 192, ), itemBuilder: (ctx, index) { final model = _.recommends[index]; return AcgItemWidget( info: _.recommends[index], tapCallback: () => _.jumpToOtherPage(model), ); }), ), ), ] else ...[ SliverToBoxAdapter( child: CErrorWidget(), ) ], 50.sliverSizeBoxH ], ), ), ), InkWell( enableFeedback: false, child: Container( margin: EdgeInsets.symmetric(horizontal: 16, vertical: 10), height: 44, alignment: Alignment.center, decoration: BoxDecoration( color: AppColors.actionRed, borderRadius: BorderRadius.circular(3), ), child: Consumer( builder: (context, value, child) => Text( _.manager.actionTitle(), style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500), ), )), onTap: () => _.gotoPlay(index: _.manager.index), ), ], ), Positioned( top: 0, left: 0, right: 0, child: _buildTopHeader(_), ) ], ); } _buildTopHeader(NovelDetailLogic _) { return GetBuilder( tag: uniqueTag, id: 'header', builder: (_) { return Container( padding: EdgeInsets.only(left: 16, right: 16, top: screen.paddingTop), height: 44 + screen.paddingTop, color: !_.showHeader ? Color(0xff0E141E) : Colors.transparent, child: Row( children: [ InkWell( enableFeedback: false, child: Image.asset('acg_noval_back.png'.acgImgPath, width: 24), onTap: () => Get.back(), ), 40.sizeBoxW, Spacer(), Text( !_.showHeader ? _.model?.title ?? '' : '', style: textStyle(16, Colors.white, FontWeight.w500), ), Spacer(), Consumer( builder: (context, manager, child) { bool col = manager.mediaInfo?.mediaStatus?.hasCollected ?? false; return FollowButton( mediaId: manager.mediaInfo?.id ?? '', isFollow: col, followType: FollowEnum.noval, successsAction: (isSuccess) { manager.updateCollectState(isSuccess); if (isSuccess) { showToast('收藏成功'); } }, ); }, ) ], ), ); }, ); } _buildNovelHeader(NovelDetailLogic _) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ NovelHeader(manager: _.manager), 18.sizeBoxH, Consumer( builder: (ctx, manager, child) => Column( children: [ 18.sizeBoxH, Row( children: [ Text( '共${_.model?.totalEpisode ?? 0}${_.model?.unit}', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w800, ), ), Spacer(), InkWell( enableFeedback: false, onTap: () => _.episodesBottomSheet(), child: Row( children: [ Text( '目录', style: TextStyle( color: Color(0xffA7A7A7), fontSize: 14, fontWeight: FontWeight.w400, ), ), 2.sizeBoxW, Image.asset( 'arrow_right_grey.webp'.commonImgPath, color: Color(0xff989898), width: 18, ) ], )), ], ), 6.sizeBoxH, ListView.separated( shrinkWrap: true, padding: EdgeInsets.zero, physics: NeverScrollableScrollPhysics(), itemCount: min(manager.allEpisodes.length, 4), separatorBuilder: (context, index) => Divider( height: .5, color: Colors.white.withValues(alpha: .05), ), itemBuilder: (BuildContext context, int index) { final epm = manager.allEpisodes[index]; return InkWell( enableFeedback: false, onTap: () => _.gotoPlay(index: index), child: Column( children: [ 12.sizeBoxH, Row( children: [ Text( '第${epm.episodeNumber ?? 1}${_.model?.unit}', style: TextStyle( color: manager.index == index ? AppColors.actionRed : Color(0xff989898), fontSize: 12, fontWeight: FontWeight.w400, ), ), //无任何权限时,前 N 集免费展示「免费」角标 if (manager.mediaInfo?.hasPermission == false && epm.inFreeEpisode) ...[ 6.sizeBoxW, const FreeBadge(), ], Spacer(), Icon(Icons.navigate_next_sharp, size: 20, color: Colors.white) ], ), 12.sizeBoxH, ], ), ); }, ), ], ), ).paddingSymmetric(horizontal: 16), ], ); } }