512 lines
18 KiB
Dart
512 lines
18 KiB
Dart
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.reverse(items 不够会从底部排),采用数据倒序方法
|
||
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,
|
||
),
|
||
);
|
||
}
|
||
}
|