初始化
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user