初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -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),
);
}
}