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

512 lines
18 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:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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_model/video_model.dart';
import 'package:hgdj/hj_page/comment/comment_alert.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/follow_button.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/scrollable_positioned_list.dart';
import 'package:hgdj/tools_base/widget/stagger_in_item.dart';
import 'package:provider/provider.dart';
import '../../alert/video/share_media_dialog.dart';
import '../../hj_utils/widget_util.dart';
import 'cartoon_read_logic.dart';
import 'widget/acg_source_manager.dart';
// 漫画阅读页面
class CartoonReadPage extends StatelessWidget {
final ACGSourceManager manager;
const CartoonReadPage({super.key, required this.manager});
@override
Widget build(BuildContext context) {
return GetBuilder<CartoonReadLogic>(
init: CartoonReadLogic(manager),
builder: (logic) => Scaffold(
body: ChangeNotifierProvider<ACGSourceManager>.value(
value: manager,
child: _buildBody(context, logic),
),
drawer: _buildDrawer(logic),
),
);
}
Widget _buildBody(BuildContext context, CartoonReadLogic logic) {
if (logic.detailInfo == null) return const LoadingCenterWidget();
if (logic.detailInfo?.id == null)
return CErrorWidget(retryOnTap: logic.loadData);
return Stack(
alignment: Alignment.center,
children: [
InkWell(
enableFeedback: false,
onTap: logic.changeShowMenu,
child: ScrollablePositionedList.builder(
scrollAction: logic.onScrollAction,
scrollOffsetController: logic.scrollOffsetCtr,
itemPositionsListener: logic.itemListener,
itemScrollController: logic.itemScrollCtr,
itemCount: logic.imgsLength,
itemBuilder: (_, index) => NetworkImageLoader(
imageUrl: logic.detailInfo?.urlSet?[index] ?? '',
fit: BoxFit.cover,
borderRadius: 0,
),
),
),
_buildHeader(logic),
_buildFooter(context, logic),
_buildLiked(logic),
_buildAutoPlay(logic),
],
);
}
// header bar
Widget _buildHeader(CartoonReadLogic logic) {
final headerHeight = screen.paddingTop + 50;
return GetBuilder<CartoonReadLogic>(
id: 'header',
builder: (_) => AnimatedPositioned(
duration: Duration(milliseconds: logic.animationTime),
top: logic.showMenu ? 0 : -headerHeight,
left: 0,
right: 0,
child: Container(
color: Colors.black,
height: headerHeight,
padding: EdgeInsets.fromLTRB(10, screen.paddingTop + 8, 10, 8),
child: Row(
children: [
InkWell(
enableFeedback: false,
onTap: () => Get.back(),
child: const Icon(Icons.arrow_back_ios_new,
size: 24, color: Colors.white),
),
10.sizeBoxW,
Expanded(
child: Text(
logic.detailInfo?.name ?? '',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
fontWeight: FontWeight.w600),
),
),
21.sizeBoxW,
Consumer<ACGSourceManager>(
builder: (ctx, manager, _) {
final 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('收藏成功');
},
);
},
),
12.sizeBoxW,
InkWell(
enableFeedback: false,
onTap: () => Get.dialog(
ShareMediaDialog(
videoModel: VideoModel()
..id = manager.mediaInfo?.id
..cover = manager.mediaInfo?.coverH,
),
),
child: Image.asset('acg_share.png'.acgImgPath, width: 24),
),
],
),
),
),
);
}
Widget _buildFooter(BuildContext context, CartoonReadLogic logic) {
return GetBuilder<CartoonReadLogic>(
id: 'footer',
builder: (_) => AnimatedPositioned(
duration: Duration(milliseconds: logic.animationTime),
bottom: logic.showMenu ? 0 : -logic.bottomHeight,
left: 0,
right: 0,
child: Column(
children: [
15.sizeBoxH,
GetBuilder<CartoonReadLogic>(
id: 'progress',
builder: (_) => AnimatedSlide(
offset: logic.showProgress ? Offset.zero : const Offset(0, 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
child: _buildProgressView(context, logic),
),
),
Container(
color: Colors.black,
padding: const EdgeInsets.fromLTRB(16, 18, 16, 25),
child: Row(
children: [
Expanded(
child: Builder(
builder: (ctx) => _buildActionItem(
title: '目录',
icon: 'cartoon_menu.png'.acgImgPath,
action: () => Scaffold.of(ctx).openDrawer(),
),
),
),
Expanded(
child: _buildActionItem(
title: '评论',
icon: 'cartoon_comment.png'.acgImgPath,
action: () => showCommentDialog(
logic.manager.mediaInfo?.id ?? '',
objType: logic.manager.mediaInfo?.commentType ?? '',
),
),
),
Expanded(
child: _buildActionItem(
title: '进度',
icon: 'cartoon_progress.png'.acgImgPath,
action: logic.toggleProgress,
),
),
Expanded(
child: _buildActionItem(
title: '自动',
icon: 'cartoon_auto_play.png'.acgImgPath,
action: () => logic.onChangeAutoPlay(true),
),
),
],
),
),
],
),
),
);
}
Widget _buildActionItem(
{String? title, String? icon, VoidCallback? action, Color? color}) {
return InkWell(
enableFeedback: false,
onTap: action,
child: Column(
children: [
Image.asset(icon ?? '', width: 24),
4.sizeBoxH,
Text(
title ?? '',
style: TextStyle(
color: color ?? Colors.white.withValues(alpha: .45),
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
);
}
// 进度条 + 上/下一篇
Widget _buildProgressView(BuildContext context, CartoonReadLogic logic) {
return Container(
color: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
InkWell(
enableFeedback: false,
onTap: () => logic.loadOtherEpisode(false),
child: Column(
children: [
Image.asset('common_back_new.png'.commonImgPath, width: 24),
4.sizeBoxH,
Text('上一篇',
style: textStyle(12, Colors.white, FontWeight.w400)),
],
),
),
13.sizeBoxW,
Expanded(
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 4,
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 8.0),
overlayShape:
const RoundSliderOverlayShape(overlayRadius: 12.0),
tickMarkShape:
const RoundSliderTickMarkShape(tickMarkRadius: 10.0),
trackShape: const RoundedRectSliderTrackShape(),
overlayColor: Colors.white.withValues(alpha: .3),
activeTrackColor: const Color(0xffFFD460),
inactiveTrackColor: Colors.white.withValues(alpha: .3),
thumbColor: const Color(0xffFFD460),
inactiveTickMarkColor: Colors.amber,
),
child: GetBuilder<CartoonReadLogic>(
id: 'slider',
builder: (_) => Slider(
min: 0,
max: 1,
value: logic.readScale,
onChanged: logic.updateReadPicIndex,
onChangeEnd: logic.onSeekEnd,
),
),
),
),
13.sizeBoxW,
InkWell(
enableFeedback: false,
onTap: () => logic.loadOtherEpisode(true),
child: Column(
children: [
Transform.rotate(
angle: pi,
child: Image.asset('common_back_new.png'.commonImgPath,
width: 24),
),
4.sizeBoxH,
Text('下一篇',
style: textStyle(12, Colors.white, FontWeight.w400)),
],
),
),
],
),
);
}
// 喜欢按钮
Widget _buildLiked(CartoonReadLogic logic) {
return GetBuilder<CartoonReadLogic>(
id: 'like',
builder: (_) => AnimatedPositioned(
duration: Duration(milliseconds: logic.animationTime),
bottom: 160,
right: logic.showMenu ? 0 : -120,
child: Consumer<ACGSourceManager>(
builder: (_, manager, __) {
final liked = manager.mediaInfo?.mediaStatus?.hasLiked == true;
return InkWell(
enableFeedback: false,
onTap: manager.onLikeAction,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: .5),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(999),
bottomLeft: Radius.circular(999),
),
),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
liked
? 'acg_like_sel.png'.acgImgPath
: 'acg_like_nor.png'.acgImgPath,
width: 24,
color: liked ? null : Colors.white,
),
5.sizeBoxW,
Text('喜欢',
style: textStyle(12, Colors.white, FontWeight.w400)),
],
),
),
);
},
),
),
);
}
// 自动播放按钮
Widget _buildAutoPlay(CartoonReadLogic logic) {
return Positioned(
bottom: 30,
child: GetBuilder<CartoonReadLogic>(
id: 'autoplay',
builder: (_) => AnimatedScale(
scale: logic.autoPlay ? 1 : 0,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
child: IgnorePointer(
ignoring: !logic.autoPlay,
child: GestureDetector(
onTap: () => logic.onChangeAutoPlay(false),
child: Container(
height: 56,
width: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: Colors.white,
),
child: Center(
child: Image.asset('cartoon_auto_play.gif'.acgImgPath,
width: 24),
),
),
),
),
),
),
);
}
// 章节抽屉
Widget _buildDrawer(CartoonReadLogic logic) {
return GetBuilder<CartoonReadLogic>(
id: 'menu',
builder: (_) => Container(
color: AppColors.primaryColor,
width: 210,
height: screen.screenHeight,
child: Column(
children: [
80.sizeBoxH,
Row(
children: [
16.sizeBoxW,
Expanded(
child: Text(
logic.manager.mediaInfo?.title ?? '',
style: TextStyle(
fontSize: 20,
color: Colors.white.withValues(alpha: 0.9),
fontWeight: FontWeight.w900,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
18.sizeBoxH,
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
color: AppColors.actionRed,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'共${logic.manager.mediaInfo?.totalEpisode}${logic.manager.mediaInfo?.getDetailUpdateString ?? ''}',
style: const TextStyle(
fontSize: 14,
color: Color(0xff141414),
fontWeight: FontWeight.w400),
),
InkWell(
enableFeedback: false,
onTap: logic.toggleSort,
child: Row(
children: [
AnimatedRotation(
turns: logic.sortType ? 0 : 0.5,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
child: Image.asset('text_play_sort.png'.acgImgPath,
color: const Color(0xff141414),
width: 16,
height: 16),
),
4.sizeBoxW,
Text(
logic.sortType ? '正序' : '倒序',
style: const TextStyle(
fontSize: 14,
color: Color(0xff141414),
fontWeight: FontWeight.w400),
),
],
),
),
],
),
),
Expanded(
child: ListView.builder(
key: ValueKey(logic.sortType),
itemCount: logic.manager.allEpisodes.length,
padding: const EdgeInsets.only(top: 6, bottom: 20),
itemBuilder: (_, index) {
// 不使用 list.reverseitems 不够会从底部排),采用数据倒序方法
final info = logic.sortType
? logic.manager.allEpisodes[index]
: logic.manager.allEpisodes[
logic.manager.allEpisodes.length - index - 1];
return StaggerInItem(
index: index,
child: GestureDetector(
onTap: () {
Get.back();
if (logic.detailInfo?.id == info.id)
return; //点的就是当前章,不重复加载
logic.loadDataWithIndex(info.id ?? '');
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
height: 40,
child: Row(
children: [
_buildDrawerItem(
//与详情/弹窗子集 title 格式一致:第X话(漫画不带 name)
'第${info.episodeNumber ?? ''}${logic.manager.mediaInfo?.unit ?? ''}',
logic.detailInfo?.id == info.id,
),
],
),
),
),
);
},
),
),
],
),
),
);
}
Widget _buildDrawerItem(String title, bool select) {
return Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 16.sp,
color: select
? AppColors.actionRed
: Colors.white.withValues(alpha: 0.55),
fontWeight: select ? FontWeight.w500 : FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
}
}