初始化
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/video/video_player_alert.dart';
|
||||
import 'package:hgdj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:hgdj/hj_utils/buy_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/hj_utils/widget_util.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
|
||||
|
||||
import '../../../../alert/video/vip_acg_dialog.dart';
|
||||
import '../../../../assets_tool/app_colors.dart';
|
||||
import '../../../mine/mine_share/mine_share_page.dart';
|
||||
|
||||
/// 帖子详情视频封面:未解锁时盖一层金币 / VIP 蒙层
|
||||
class PostDetailVideoContent extends StatefulWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailVideoContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
State<PostDetailVideoContent> createState() => _PostDetailVideoContentState();
|
||||
}
|
||||
|
||||
class _PostDetailVideoContentState extends State<PostDetailVideoContent> {
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPlayable = model.isVideo() || model.isSeedPost();
|
||||
if (!isPlayable || model.sourceURL?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
final isLocked = needPostCoin(model);
|
||||
//非免费区的金币帖走金币解锁,其余走 VIP 解锁
|
||||
final isCoin = !(model.freeArea ?? false) && model.originCoins != 0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (needPostCoin(model)) return;
|
||||
_openPlayer();
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 220),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 391 / 220,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model.cover ?? '',
|
||||
width: double.infinity,
|
||||
height: double.infinity),
|
||||
Image.asset('circle_play.webp'.videoPath,
|
||||
width: 30, height: 30),
|
||||
//未解锁蒙层
|
||||
if (isLocked)
|
||||
Positioned.fill(
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: .6),
|
||||
child: isCoin ? _coinMask() : _vipMask(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (model.isSeedPost()) 10.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//金币解锁蒙层
|
||||
Widget _coinMask() {
|
||||
final coins = model.originCoins ?? 0;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
EasyRichText(
|
||||
'你只需花费 $coins 金币',
|
||||
defaultStyle: textStyle(16, Colors.white, FontWeight.w400),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '$coins',
|
||||
style: textStyle(16, AppColors.actionRed, FontWeight.w400)),
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
const Text('即可解锁继续查看',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400)),
|
||||
16.sizeBoxH,
|
||||
_btn(
|
||||
'立即解锁',
|
||||
width: 84,
|
||||
colors: const [Color(0xffFF6E6E), Color(0xffFF4D4D)],
|
||||
onTap: () async {
|
||||
//购买期间可能已被其他入口解锁,点的时候再判一次
|
||||
if (!needPostCoin(model)) {
|
||||
_openPlayer();
|
||||
return;
|
||||
}
|
||||
if (!await _pay()) return;
|
||||
model.vidStatus?.hasPaid = true;
|
||||
if (mounted) setState(() {});
|
||||
_openPlayer();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//VIP 解锁蒙层
|
||||
Widget _vipMask() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('本视频需开通VIP免费观看',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400)),
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_btn(
|
||||
'邀请得3日VIP',
|
||||
width: 110,
|
||||
colors: const [Color(0xffFFE8BE), Color(0xffE6B764)],
|
||||
textColor: const Color(0xff694923),
|
||||
onTap: () => Get.to(() => MineSharePage()),
|
||||
),
|
||||
18.sizeBoxW,
|
||||
_btn(
|
||||
'开通VIP免费看',
|
||||
width: 115,
|
||||
colors: const [Color(0xffFF6E6E), Color(0xffFF4D4D)],
|
||||
onTap: () => Get.dialog(VipACGDialog()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//蒙层上的胶囊按钮
|
||||
Widget _btn(
|
||||
String text, {
|
||||
required double width,
|
||||
required List<Color> colors,
|
||||
required VoidCallback onTap,
|
||||
Color textColor = Colors.white,
|
||||
}) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: colors),
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Text(text, style: textStyle(12, textColor, FontWeight.w400)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _pay() async {
|
||||
bool isOk = false;
|
||||
await PayManager().buy(
|
||||
model.id,
|
||||
ProductType.media,
|
||||
source: 'post_cover',
|
||||
onSuccess: (data) {
|
||||
globalStore.updateUserInfo();
|
||||
isOk = true;
|
||||
},
|
||||
);
|
||||
return isOk;
|
||||
}
|
||||
|
||||
void _openPlayer() => Get.to(() => VideoPlayerAlert(model.realVideoUrl));
|
||||
}
|
||||
|
||||
/// 帖子详情多图封面:竖向拼接,只有首尾切圆角
|
||||
class PostDetailCoverContent extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailCoverContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//有视频源时封面已在视频区展示,这里剔除避免重复
|
||||
if (model.sourceURL?.isNotEmpty == true)
|
||||
model.seriesCover?.remove(model.cover);
|
||||
final covers = model.seriesCover ?? [];
|
||||
return Column(
|
||||
children: [
|
||||
if (covers.isNotEmpty)
|
||||
Wrap(
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (final (index, url) in covers.indexed)
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => ImageBrowserPage.open(covers, index: index),
|
||||
child: ClipRRect(
|
||||
borderRadius: _radius(index, covers.length),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: url,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fill,
|
||||
borderRadius: 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
BorderRadius _radius(int index, int total) {
|
||||
const radius = Radius.circular(12);
|
||||
if (total == 1) return const BorderRadius.all(radius);
|
||||
if (index == 0)
|
||||
return const BorderRadius.only(topLeft: radius, topRight: radius);
|
||||
if (index == total - 1)
|
||||
return const BorderRadius.only(bottomLeft: radius, bottomRight: radius);
|
||||
return BorderRadius.zero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../../alert/video/share_media_dialog.dart';
|
||||
import '../../../mine/collec_history_buy/col_his_buy_sub_page.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
/// 帖子详情操作栏:浏览量 / 点赞 / 评论 / 分享
|
||||
class PostDetailOperationView extends StatefulWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailOperationView({super.key, required this.model});
|
||||
|
||||
@override
|
||||
State<PostDetailOperationView> createState() =>
|
||||
_PostDetailOperationViewState();
|
||||
}
|
||||
|
||||
class _PostDetailOperationViewState extends State<PostDetailOperationView> {
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 24,
|
||||
margin: EdgeInsets.only(bottom: 18.h),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 浏览量
|
||||
_iconLabel(
|
||||
icon: "eye_grey.webp".commonImgPath,
|
||||
gap: 2,
|
||||
text: model.pageViewCount?.countStr ?? "",
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
40.sizeBoxW,
|
||||
// 点赞
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _toggleLike,
|
||||
child: _iconLabel(
|
||||
icon: model.vidStatus?.hasLiked == true
|
||||
? "like_red.png".commonImgPath
|
||||
: "like_empty_gray.png".communityPath,
|
||||
gap: 5.w,
|
||||
text: model.likeCount?.countOr("喜欢") ?? '',
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
40.sizeBoxW,
|
||||
// 评论量
|
||||
_iconLabel(
|
||||
icon: "count_msg_gray.png".communityPath,
|
||||
gap: 2,
|
||||
text: model.commentCount?.countStr ?? "",
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xff999999),
|
||||
),
|
||||
40.sizeBoxW,
|
||||
// 分享
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Get.dialog(ShareMediaDialog(videoModel: model)),
|
||||
child: _iconLabel(
|
||||
icon: 'share_grey.png'.commonImgPath,
|
||||
iconSize: 16,
|
||||
gap: 4.w,
|
||||
text: '分享',
|
||||
fontSize: 10.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 图标 + 文案的横向组合
|
||||
Widget _iconLabel({
|
||||
required String icon,
|
||||
required String text,
|
||||
required double fontSize,
|
||||
double iconSize = 20,
|
||||
double gap = 2,
|
||||
Color color = const Color(0xff989898),
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(icon, width: iconSize, height: iconSize),
|
||||
gap.sizeBoxW,
|
||||
Text(text, style: TextStyle(color: color, fontSize: fontSize)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 点赞 / 取消点赞
|
||||
void _toggleLike() async {
|
||||
final wasLiked = model.vidStatus?.hasLiked ?? false;
|
||||
final count = model.likeCount ?? 0;
|
||||
final res = wasLiked
|
||||
? await CommonService.cancelLike(
|
||||
model.id ?? '', LoadDataType.post.apiType)
|
||||
: await CommonService.sendLike(
|
||||
model.id ?? '', LoadDataType.post.apiType);
|
||||
if (!res || !mounted) return;
|
||||
model.vidStatus?.hasLiked = !wasLiked;
|
||||
model.likeCount = max(0, count + (wasLiked ? -1 : 1));
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
/// 帖子金币解锁弹窗:解锁成功回传 true,点空白关闭交给 barrier
|
||||
class PostDetailUnlockDialog extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailUnlockDialog({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => PayManager().buy(
|
||||
model.id,
|
||||
ProductType.media,
|
||||
source: 'post_unlock',
|
||||
onSuccess: (data) {
|
||||
globalStore.updateUserInfo();
|
||||
Get.back(result: true);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color.fromRGBO(255, 238, 220, 1),
|
||||
Color.fromRGBO(249, 219, 191, 1)
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('post_detail_coin.png'.communityPath,
|
||||
width: 30, height: 30),
|
||||
Text(
|
||||
'${model.videoCoin()}金币解锁继续',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff370000),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
|
||||
|
||||
import '../../../../tools_base/richTextParsing/html_parser.dart';
|
||||
import '../../../../tools_base/widget/shrink_wrap.dart';
|
||||
import '../../community_tag_page/community_tag_page.dart';
|
||||
|
||||
/// 黄游帖的游戏介绍
|
||||
class GameDetailWordContent extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const GameDetailWordContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (model.newsType != 'ADULT_GAME') return const SizedBox.shrink();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('游戏介绍', style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||
18.sizeBoxH,
|
||||
Text(model.content ?? '',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 帖子正文:标题 + 标签 + 富文本 / 纯文本内容
|
||||
class PostDetailWordContent extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailWordContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (model.newsType == 'ADULT_GAME') return const SizedBox.shrink();
|
||||
final contentStyle =
|
||||
TextStyle(color: const Color(0xFFDCDCDC), fontSize: 14.sp);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
14.sizeBoxH,
|
||||
//标题
|
||||
if (model.title?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 1.h),
|
||||
child: Text(model.title ?? '', style: contentStyle)),
|
||||
//标签
|
||||
_tags(),
|
||||
12.h.sizeBoxH,
|
||||
Divider(height: .5, color: Colors.white.withValues(alpha: .05)),
|
||||
18.h.sizeBoxH,
|
||||
//正文:富文本优先,没有富文本才走纯文本
|
||||
if (model.richText?.isNotEmpty == true)
|
||||
_richText()
|
||||
else if (model.content?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 18.h),
|
||||
child: Text(model.content ?? '', style: contentStyle)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _richText() {
|
||||
return DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
children: HtmlParser(
|
||||
width: double.infinity,
|
||||
onLinkTap: pushToPageByLink,
|
||||
imgLinkTap: (url) => ImageBrowserPage.open([url]),
|
||||
).parse(model.richText ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tags() {
|
||||
final tags = model.tags ?? [];
|
||||
if (tags.isEmpty) return const SizedBox();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: ShrinkWrap(
|
||||
spacing: 0,
|
||||
runSpacing: 6,
|
||||
maxLines: 1,
|
||||
children: tags
|
||||
.map((e) => InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(() => CommunityTagDetailPage(model: e)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 3, 8, 3),
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(3)),
|
||||
child: Text('#${e.name}',
|
||||
style: const TextStyle(
|
||||
color: Color(0x8CFFFFFF), fontSize: 12)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
import '../../../../hj_model/video_model.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'post_detail_unlock_dialog.dart';
|
||||
|
||||
/// 种子帖 / 黄游帖的资源信息与下载入口
|
||||
class PostResourceInfoWidget extends StatefulWidget {
|
||||
final VideoModel videoModel;
|
||||
|
||||
const PostResourceInfoWidget({super.key, required this.videoModel});
|
||||
|
||||
@override
|
||||
State<PostResourceInfoWidget> createState() => _PostResourceInfoWidgetState();
|
||||
}
|
||||
|
||||
class _PostResourceInfoWidgetState extends State<PostResourceInfoWidget> {
|
||||
VideoModel get model => widget.videoModel;
|
||||
|
||||
/// 是否还需要花金币解锁
|
||||
bool get _needLock {
|
||||
if (model.freeArea == true) return false;
|
||||
final me = globalStore.meInfo;
|
||||
//金币视频限免期内:全场免费 或 50 金币以内免费
|
||||
final isFreeTime = DateTimeUtil.calTime3(me?.goldVideoFreeExpire) > 0;
|
||||
if (isFreeTime && (me?.allGoldVideoFree == true || (model.coins ?? 0) < 50))
|
||||
return false;
|
||||
return model.isCoinVideo() &&
|
||||
!globalStore.isMe(model.publisher?.uid) &&
|
||||
!(model.vidStatus?.hasPaid ?? false);
|
||||
}
|
||||
|
||||
String get _buyDesc =>
|
||||
_needLock ? '${model.realCoins ?? 0}金币解锁 即可下载种子' : '开通VIP 即可下载种子';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSeed = model.newsType == 'SEED_LINK';
|
||||
if (!isSeed && model.newsType != 'ADULT_GAME') return const SizedBox();
|
||||
|
||||
const descStyle =
|
||||
TextStyle(color: Color(0xff999999), fontSize: 14, height: 1.5);
|
||||
//未解锁、或非 VIP 的非免费区内容,都只能看解锁入口
|
||||
final isLocked =
|
||||
_needLock || (!globalStore.isVIP && model.freeArea != true);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//种子帖的影片信息与下载说明
|
||||
if (isSeed) ...[
|
||||
if ((model.seedSize ?? 0) > 0)
|
||||
Text('[影片大小]:${model.seedSizeDesc}', style: descStyle),
|
||||
if ((model.seedPlayTime ?? 0) > 0)
|
||||
Text('[影片时长]:${model.seedPlayTimeDesc}', style: descStyle),
|
||||
const Text(
|
||||
'[下载说明]:解锁后请复制种子的下载链接到手机自带浏览器(谷歌/UC/夸克/火狐 等)打开!或使用BT软件(迅雷/Vuze/uTorrent/BitTorrent 等)进行下载.',
|
||||
style: descStyle,
|
||||
),
|
||||
const Text('[温馨提示]:下载后请勿传播分享,仅供个人欣赏哟~~', style: descStyle),
|
||||
18.sizeBoxH,
|
||||
],
|
||||
//解锁入口 / 复制链接
|
||||
Center(
|
||||
child: isLocked
|
||||
? InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _showUnlock,
|
||||
child: Text(
|
||||
_buyDesc,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.primaryHighColor,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
)
|
||||
: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _copyLink,
|
||||
child: Image.asset('link_copy_icon.webp'.communityPath,
|
||||
width: 180, height: 32),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 需要金币的弹解锁弹窗,否则去充值页开 VIP
|
||||
void _showUnlock() async {
|
||||
if (!_needLock) {
|
||||
pushToWalletPage(tabPosition: 0);
|
||||
return;
|
||||
}
|
||||
final result = await Get.dialog(
|
||||
PostDetailUnlockDialog(model: model),
|
||||
barrierColor: const Color.fromRGBO(14, 20, 30, 0.6),
|
||||
);
|
||||
if (result == null || !mounted) return;
|
||||
model.vidStatus?.hasPaid = true;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _copyLink() {
|
||||
Clipboard.setData(ClipboardData(text: model.seedRealLink));
|
||||
showToast('链接地址复制成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/comment/comment_mixin.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
|
||||
import '../../../hj_utils/history_util.dart';
|
||||
import 'post_detail_page.dart';
|
||||
|
||||
class PostDetailLogic extends GetxController with CommentMixin {
|
||||
// ===== 状态 =====
|
||||
bool isLoading = false;
|
||||
|
||||
// ===== 数据 =====
|
||||
final PostDetailPageArgument args;
|
||||
VideoModel? model;
|
||||
|
||||
// ===== Controller =====
|
||||
final scrollCtr = ScrollController();
|
||||
|
||||
PostDetailLogic(this.args);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
id = args.id;
|
||||
objType = 'video';
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
initData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// scrollCtr 在本类创建,需自行释放;commentController 由 CommentMixin.onClose 处理
|
||||
scrollCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
//详情与评论并行拉,两个都回来才收 loading
|
||||
void initData() {
|
||||
isLoading = true;
|
||||
update();
|
||||
Future.wait([fetchDetail(), fetchComments(args.id ?? '')]).then((_) {
|
||||
isLoading = false;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
Future fetchDetail() async {
|
||||
final res = await VidService.getDetail(args.id ?? '');
|
||||
if (res != null) {
|
||||
model = res;
|
||||
HistoryUtil.insert(model!, MediaStyle.Community);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
/// 上下篇切换:原地换 id 重拉,不 push 新页
|
||||
/// 配套数据是 args.videoModels(各入口都在传),但翻页入口还没接,别当死代码删
|
||||
void nextOrPrePost(String? newId) {
|
||||
args.id = newId;
|
||||
id = newId;
|
||||
page = 1;
|
||||
comments.clear();
|
||||
initData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/comment/cell/comment_item.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_grid_view_widget.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
import '../../comment/widget/comment_foot_view.dart';
|
||||
import '../../home/widget/quick_search_view.dart';
|
||||
import '../widget/post_user_info_view.dart';
|
||||
import 'detail_widget/post_detail_cover_content.dart';
|
||||
import 'detail_widget/post_detail_operation_view.dart';
|
||||
import 'detail_widget/post_detail_word_content.dart';
|
||||
import 'detail_widget/post_resource_info_widget.dart';
|
||||
import 'post_detail_logic.dart';
|
||||
|
||||
/// 过滤掉广告位,只留真实帖子
|
||||
List<VideoModel>? _filters(List<VideoModel>? videoModel) {
|
||||
return (videoModel ?? [])
|
||||
.where((e) => !e.isRandomAd() && !e.isAdsArr())
|
||||
.toList();
|
||||
}
|
||||
|
||||
class PostDetailPageArgument {
|
||||
final List<VideoModel>? videoModels;
|
||||
String? id;
|
||||
|
||||
PostDetailPageArgument({List<VideoModel>? models, this.id})
|
||||
: videoModels = _filters(models);
|
||||
}
|
||||
|
||||
class PostDetailPage extends StatefulWidget {
|
||||
final PostDetailPageArgument argument;
|
||||
|
||||
const PostDetailPage({super.key, required this.argument});
|
||||
|
||||
@override
|
||||
State<PostDetailPage> createState() => _PostDetailPageState();
|
||||
}
|
||||
|
||||
// 详情页能点头像进用户主页,主页作品列表又能开帖子详情(A→B→A),两份会并存,
|
||||
// 所以 logic 必须按实例隔离;不能省掉 tag 让它们共用一个
|
||||
class _PostDetailPageState extends State<PostDetailPage> with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<PostDetailLogic>(
|
||||
init: PostDetailLogic(widget.argument),
|
||||
tag: uniqueTag,
|
||||
builder: (logic) {
|
||||
final model = logic.model;
|
||||
//置顶评论下标:getter 内部要遍历评论,别丢进 itemBuilder 里每条算一遍
|
||||
final topIndex = logic.topPinnedCommentIndex;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
titleSpacing: 18.w,
|
||||
automaticallyImplyLeading: false,
|
||||
title: _titleView(logic)),
|
||||
body: logic.isLoading || model == null
|
||||
? SizedBox(height: 300, child: LoadingCenterWidget())
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18.w),
|
||||
child: pullYsRefresh(
|
||||
enablePullDown: false,
|
||||
onInit: (refreshCtr) => logic.refreshCtr = refreshCtr,
|
||||
onLoading: (_) =>
|
||||
logic.fetchComments(logic.args.id ?? ''),
|
||||
child: CustomScrollView(
|
||||
controller: logic.scrollCtr,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: 14.h.sizeBoxH),
|
||||
//发帖人信息
|
||||
SliverToBoxAdapter(
|
||||
child: PostUserInfoView(
|
||||
model: model, isDetailStyle: true)),
|
||||
//正文
|
||||
SliverToBoxAdapter(
|
||||
child: PostDetailWordContent(model: model)),
|
||||
//富文本帖自带图,不再单独铺封面
|
||||
if (model.richText?.isNotEmpty != true)
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
PostDetailCoverContent(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: GameDetailWordContent(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: PostDetailVideoContent(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: PostResourceInfoWidget(
|
||||
videoModel: model)),
|
||||
SliverToBoxAdapter(child: 16.sizeBoxH),
|
||||
//点赞/收藏/分享
|
||||
SliverToBoxAdapter(
|
||||
child: PostDetailOperationView(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: AdsGridViewWidget(
|
||||
7,
|
||||
accordingAdsType: true,
|
||||
),
|
||||
),
|
||||
//评论数 + 快捷搜索词
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${model.commentCount?.countStr}条评论',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white
|
||||
.withValues(alpha: 0.9)),
|
||||
),
|
||||
if (logic.quickSearchList.isNotEmpty) ...[
|
||||
18.sizeBoxH,
|
||||
QuickSearchView(logic.quickSearchList)
|
||||
]
|
||||
],
|
||||
).paddingOnly(bottom: 14),
|
||||
),
|
||||
//评论列表
|
||||
if (logic.comments.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 220, child: CErrorWidget()))
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: logic.comments.length,
|
||||
itemBuilder: (_, index) => CommentItem(
|
||||
logic.comments[index],
|
||||
objId: model.id ?? '',
|
||||
//置顶那条上方插一组 Banner
|
||||
topBanners: index == topIndex &&
|
||||
logic.commentTopBanners.isNotEmpty
|
||||
? logic.commentTopBanners
|
||||
: null,
|
||||
replyHandler: (comment, {reply}) =>
|
||||
logic.readyReplyComment(comment,
|
||||
replyModel: reply),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
//底部评论输入条
|
||||
Obx(
|
||||
() => CommentFootView(
|
||||
video: model,
|
||||
showCollect: true,
|
||||
onComment: (isSend) =>
|
||||
logic.readyReplyComment(null, isSend: isSend),
|
||||
isSending: logic.isSendingMsg.value,
|
||||
textCtr: logic.commentController,
|
||||
imgPath: logic.selectedImagePath.value,
|
||||
onImgChange: (value) =>
|
||||
logic.selectedImagePath.value = value,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//标题栏:未滚动 / 游戏帖显示固定标题,滚过 60 后换成发帖人信息
|
||||
Widget _titleView(PostDetailLogic logic) {
|
||||
final scrollCtr = logic.scrollCtr;
|
||||
return ListenableBuilder(
|
||||
listenable: scrollCtr,
|
||||
builder: (_, __) {
|
||||
final isGame = logic.model?.newsType == 'ADULT_GAME';
|
||||
if (!scrollCtr.hasClients || scrollCtr.offset < 60 || isGame) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
_backBtn(),
|
||||
Center(
|
||||
child: Text(switch (logic.model?.newsType) {
|
||||
'SEED_LINK' => '种子详情',
|
||||
'ADULT_GAME' => '游戏详情',
|
||||
_ => '帖子详情',
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
_backBtn(),
|
||||
4.sizeBoxW,
|
||||
Expanded(
|
||||
child: PostUserInfoView(
|
||||
model: logic.model!, isDetailStyle: true, avatarSize: 30)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _backBtn() {
|
||||
return GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: const Icon(Icons.arrow_back_ios, size: 18, color: Colors.white),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user