Files
huangguo_android/lib/hj_page/cartoon/novel_detail_page.dart
T
2026-09-15 15:44:13 +07:00

294 lines
11 KiB
Dart

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<NovelDetailPage> createState() => _NovelDetailPageState();
}
// 详情页可经「推荐 → 再开详情页」成环并存,用 UniqueTagMixin 给每个实例唯一 tag
class _NovelDetailPageState extends State<NovelDetailPage> with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return GetBuilder<NovelDetailLogic>(
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<ACGSourceManager>(
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<ACGSourceManager>(
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<NovelDetailLogic>(
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<ACGSourceManager>(
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<ACGSourceManager>(
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),
],
);
}
}