Files
2026-09-15 15:44:13 +07:00

594 lines
21 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<CartoonDetailPage> createState() => _CartoonDetailPageState();
}
// 详情页可经「推荐页 → 再开详情页」成环并存,用 UniqueTagMixin 给每个实例唯一 tag
class _CartoonDetailPageState extends State<CartoonDetailPage>
with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xff05000E),
body: GetBuilder<CartoonDetailLogic>(
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<MediaStyle> _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<ACGSourceManager>(
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<ACGSourceManager>(
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<MediaStyle> 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<ACGSourceManager>(
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<CartoonDetailLogic>(
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<ACGSourceManager>(
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('收藏成功');
}
},
);
},
);
}
}