初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/api_service/common_service.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/hj_utils/history_util.dart';
import 'package:hgdj/hj_utils/const.dart';
import '../../alert/video/video_buy_bottom_sheet.dart';
class AcgGameDetailLogic extends GetxController
with GetTickerProviderStateMixin {
// 外部传入:游戏(资源) ID
final String gameId;
AcgGameDetailLogic({this.gameId = ''});
// Tab:相关推荐 / 评论
final List<String> tabs = ['相关推荐', '评论'];
late TabController tabCtr = TabController(length: tabs.length, vsync: this);
// 详情数据 + 评论总数(评论区回调实时刷新角标)
VideoModel? detailInfo;
RxInt totalCtCount = 0.obs;
@override
void onReady() {
super.onReady();
loadData();
}
@override
void onClose() {
tabCtr.dispose(); // 释放 TabController,避免 ticker 泄漏
super.onClose();
}
loadData() async {
detailInfo = await VidService.getDetail(gameId);
if (detailInfo != null) {
totalCtCount.value = detailInfo?.commentCount ?? 0;
HistoryUtil.insert(detailInfo!, MediaStyle.Game);
}
detailInfo ??= VideoModel();
update();
}
////金币解锁
showAlert() async {
if (detailInfo?.lockType == 1) {
var ret = await Get.bottomSheet(VideoBuyBottomSheet(video: detailInfo!));
if (ret == true) {
detailInfo?.vidStatus?.hasPaid = true;
update(['link']);
Clipboard.setData(ClipboardData(text: detailInfo?.seedLinkUrl ?? ""));
showToast("链接地址复制成功");
}
}
}
onLinkClickAction() {
Clipboard.setData(ClipboardData(text: detailInfo?.seedLinkUrl ?? ""));
showToast("链接地址复制成功");
}
//收藏
onCollectionAction() async {
String type = 'SEED_LINK';
String? objID = detailInfo?.id;
bool hasCollected = detailInfo?.vidStatus?.hasCollected ?? false;
if (hasCollected) {
final success = await MineService.postCollect(objID, type, false);
if (success) {
detailInfo?.collectCount = (detailInfo?.collectCount ?? 1) - 1;
detailInfo?.vidStatus?.hasCollected = false;
detailInfo?.collectCount = max(0, detailInfo?.collectCount ?? 0);
showToast("已取消收藏");
}
} else {
final success = await MineService.postCollect(objID, type, true);
if (success) {
detailInfo?.collectCount = (detailInfo?.collectCount ?? 0) + 1;
detailInfo?.vidStatus?.hasCollected = true;
detailInfo?.collectCount = max(0, detailInfo?.collectCount ?? 0);
showToast("收藏成功");
}
}
update(['collect']);
}
//点赞
onLikeAction() async {
String type = 'SEED_LINK';
String? objID = detailInfo?.id;
bool hasLiked = detailInfo?.vidStatus?.hasLiked ?? false;
if (hasLiked) {
await CommonService.cancelLike(objID, type);
detailInfo?.likeCount = (detailInfo?.likeCount ?? 1) - 1;
detailInfo?.likeCount = max(0, detailInfo?.likeCount ?? 0);
detailInfo?.vidStatus?.hasLiked = false;
showToast("取消点赞");
} else {
await CommonService.sendLike(objID, type);
detailInfo?.likeCount = (detailInfo?.likeCount ?? 0) + 1;
detailInfo?.likeCount = max(0, detailInfo?.likeCount ?? 0);
detailInfo?.vidStatus?.hasLiked = true;
showToast("点赞成功");
}
update(['like']);
}
}
+393
View File
@@ -0,0 +1,393 @@
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
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/extension/extensions.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/comment/comment_views.dart';
import 'package:hgdj/hj_page/find/acg_game_detail_logic.dart';
import 'package:hgdj/hj_page/find/acg_game_recommend_page.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/hj_utils/sliver_delegate.dart';
import 'package:hgdj/tools_base/banner/ads_banner_widget.dart';
import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import 'package:mobkit_dashed_border/mobkit_dashed_border.dart';
import '../../hj_utils/widget_util.dart';
//acg游戏详情
class AcgGameDetailPage extends StatefulWidget {
final String id;
const AcgGameDetailPage({super.key, this.id = ''});
@override
State<AcgGameDetailPage> createState() => _AcgGameDetailPageState();
}
class _AcgGameDetailPageState extends State<AcgGameDetailPage>
with UniqueTagMixin {
// 详情可自我堆叠(相关推荐→再开详情),uniqueTag 由 UniqueTagMixin 提供,隔离多实例 logic
late final AcgGameDetailLogic logic = AcgGameDetailLogic(gameId: widget.id);
@override
Widget build(BuildContext context) {
return GetBuilder<AcgGameDetailLogic>(
init: logic,
tag: uniqueTag,
builder: (_) {
return Scaffold(
appBar: AppBar(title: Text(_.detailInfo?.title ?? '')),
body: () {
if (_.detailInfo == null) return LoadingCenterWidget();
if (_.detailInfo?.id == null) return CErrorWidget();
return _buildSourceContent();
}(),
);
},
);
}
Widget _buildSourceContent() {
return ExtendedNestedScrollView(
onlyOneScrollInBody: true,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
//图片
if (logic.detailInfo?.seriesCover?.isNotEmpty == true)
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: ImageCollectionWidget(
imgs: logic.detailInfo?.seriesCover,
showCIndicator: true,
showIndex: false,
aspectRatio: 343 / 143,
),
),
),
_buildContent(),
SliverPersistentHeader(
pinned: true,
delegate: MySliverDelegate(
maxHeight: 44,
minHeight: 44,
child: Container(
color: AppColors.primaryColor,
child: _buildSection(),
),
),
),
];
},
body: TabBarView(
controller: logic.tabCtr,
children: [
AcgGameRecommendPage(mediaModel: logic.detailInfo).keepAlive,
CommentView(
logic.gameId,
objType: "video",
totalCallback: (value) {
logic.totalCtCount.value = value;
},
).keepAlive,
],
),
);
}
//推荐和评论
Widget _buildSection() {
return TabBar(
padding: EdgeInsets.only(bottom: 6),
tabAlignment: TabAlignment.start,
controller: logic.tabCtr,
isScrollable: true,
labelColor: Colors.white,
labelStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
unselectedLabelColor: Color(0xffE0E0E0),
unselectedLabelStyle:
const TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
indicator: CustomIndicator(
width: 16,
height: 4,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(3),
topRight: Radius.circular(3),
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
),
),
tabs: List.generate(
logic.tabs.length,
(index) => Container(
padding: EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(logic.tabs[index]),
if (index == 1) ...[
Obx(
() => Container(
padding: EdgeInsets.only(left: 2),
child: Text(
'${logic.totalCtCount.value == 0 ? '' : logic.totalCtCount.value}',
style:
textStyle(12, AppColors.actionRed, FontWeight.w500),
),
),
),
]
],
),
),
),
);
}
Widget _buildContent() {
return SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
10.sizeBoxH,
Text(
'游戏简介:',
style: const TextStyle(
fontSize: 16,
color: Color(0xffDCDCDC),
fontWeight: FontWeight.w500,
),
),
18.sizeBoxH,
Text(
logic.detailInfo?.content ?? '',
style: const TextStyle(
fontSize: 14,
color: Color(0xffDCDCDC),
fontWeight: FontWeight.w400,
),
),
18.sizeBoxH,
_buildRightWidget(),
if (logic.detailInfo?.tags != null &&
logic.detailInfo?.tags?.isNotEmpty == true) ...[
20.sizeBoxH,
Wrap(
spacing: 10,
runSpacing: 0,
children:
List.generate(logic.detailInfo?.tags?.length ?? 0, (index) {
final tag = logic.detailInfo?.tags?[index];
return Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.circular(3),
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
'#${tag?.name ?? ''}',
style: TextStyle(
fontSize: 12,
color: Color(0xff989898),
),
),
);
}),
),
],
20.sizeBoxH,
_buildInfoStatus(),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
],
),
),
);
}
Widget _buildRightWidget() {
return GetBuilder<AcgGameDetailLogic>(
tag: uniqueTag,
id: 'link',
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_.detailInfo?.lockType == 0) ...[
//无需解锁
Row(
children: [
Spacer(),
InkWell(
enableFeedback: false,
onTap: () => logic.onLinkClickAction(),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
border: DashedBorder.fromBorderSide(
dashLength: 3,
side: BorderSide(
color: Colors.white.withValues(alpha: .4),
width: 1,
),
),
borderRadius: BorderRadius.circular(32),
),
child: EasyRichText(
'点击复制「种子资源」',
defaultStyle: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w400),
patternList: [
EasyRichTextPattern(
targetString: '「种子资源」',
style: TextStyle(color: Color(0xffE1351F)),
),
],
),
),
),
Spacer(),
],
)
] else ...[
//金币解锁
InkWell(
enableFeedback: false,
onTap: () => logic.showAlert(),
child: Container(
height: 124,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
border: DashedBorder.fromBorderSide(
dashLength: 3,
side: BorderSide(
color: Color(0xffC781FF),
width: 1,
),
),
borderRadius: BorderRadius.circular(8),
),
child: Container(
alignment: Alignment.center,
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white.withValues(alpha: .1),
),
child: Text(
'内容已隐藏,解锁后查看',
style: textStyle(14, Colors.white, FontWeight.w400),
),
),
),
),
18.sizeBoxH,
InkWell(
enableFeedback: false,
onTap: () => logic.showAlert(),
child: Container(
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: AppColors.actionRed,
),
child: Text(
'${_.detailInfo?.newsType == "SEED_LINK" ? _.detailInfo?.originCoins ?? 0 : _.detailInfo?.coins ?? 0}金币/立即解锁',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
)
]
],
),
);
}
//观看/点赞/解锁
Widget _buildInfoStatus() {
return Row(
children: [
//观看
_buildStatusItem('eye_white.webp'.commonImgPath,
logic.detailInfo?.playCount.countStr ?? '', false),
30.sizeBoxW,
//收藏
GetBuilder<AcgGameDetailLogic>(
tag: uniqueTag,
id: 'collect',
builder: (_) => InkWell(
enableFeedback: false,
onTap: logic.onCollectionAction,
child: _buildStatusItem(
logic.detailInfo?.hasCollected == true
? 'collect_red.png'.commonImgPath
: 'collect_path.png'.commonImgPath,
logic.detailInfo?.collectCount?.countOr("0") ?? '',
logic.detailInfo?.hasCollected == true,
),
),
),
30.sizeBoxW,
//喜欢
GetBuilder<AcgGameDetailLogic>(
tag: uniqueTag,
id: 'like',
builder: (_) => InkWell(
enableFeedback: false,
onTap: logic.onLikeAction,
child: _buildStatusItem(
logic.detailInfo?.hasLiked == true
? 'like_red.png'.commonImgPath
: 'like_empty.png'.communityPath,
logic.detailInfo?.likeCount?.countOr("0") ?? '',
logic.detailInfo?.hasLiked == true,
),
),
),
30.sizeBoxW,
_buildStatusItem(
'locked_white.png'.communityPath,
logic.detailInfo?.purchaseCount.countStr ?? '',
false,
),
],
);
}
Widget _buildStatusItem(String imgPath, String count, bool isSelected) {
return Row(
children: [
Image.asset(
imgPath,
width: 20,
color: isSelected ? null : Color(0xff989898),
),
2.sizeBoxW,
Text(
count,
style: TextStyle(
fontSize: 12,
color: isSelected ? AppColors.actionRed : Color(0xff989898),
),
),
],
);
}
}
@@ -0,0 +1,35 @@
import 'package:hgdj/hj_model/home/video_list_model.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/api_service/common_service.dart';
import 'package:hgdj/tools_base/base_list_controller.dart';
class AcgGameRecommendLogic extends ListBaseLogic {
VideoModel? mediaModel;
AcgGameRecommendLogic({this.mediaModel});
@override
onReady() {
super.onReady();
loadData();
}
loadData({int pageNum = 1, bool showLoading = false, int size = 10}) async {
dataList ??= [];
VideoListResp? resp = await CommonService.getRecommendList(
pageNumber: pageNum, pageSize: 10, tagId: mediaModel?.tags?.first.id);
if (resp != null) {
currentPage = pageNum;
if (pageNum == 1) dataList?.clear();
dataList?.addAll(resp.videos ?? []);
}
update();
resp?.hasNext == true
? refreshCtr?.loadComplete()
: refreshCtr?.loadNoData();
refreshCtr?.refreshCompleted();
}
loadMoreData() async => loadData(pageNum: currentPage + 1);
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/find/acg_game_recommend_logic.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 '../home/home_cell_style/video_simple_cell.dart';
//游戏推荐页面
class AcgGameRecommendPage extends StatefulWidget {
final VideoModel? mediaModel;
const AcgGameRecommendPage({super.key, this.mediaModel});
@override
State<AcgGameRecommendPage> createState() => _AcgGameRecommendPageState();
}
class _AcgGameRecommendPageState extends State<AcgGameRecommendPage>
with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return GetBuilder<AcgGameRecommendLogic>(
tag: uniqueTag,
init: AcgGameRecommendLogic(mediaModel: widget.mediaModel),
builder: (_) {
if (_.dataList == null) return LoadingCenterWidget();
if (_.dataList?.isEmpty == true)
return CErrorWidget(
errorMsg: '暂无数据',
retryOnTap: () => _.loadData(),
);
return pullYsRefresh(
onInit: (ctr) => _.refreshCtr = ctr,
onLoading: (refreshcontroller) => _.loadMoreData(),
enablePullDown: false,
child: GridView.builder(
physics: ClampingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, 12, 16, 40),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 5,
childAspectRatio: 179 / 147,
),
itemCount: _.dataList?.length ?? 0,
itemBuilder: (context, index) {
VideoModel? model = _.dataList?[index];
return VideoSimpleCell(
videoModel: model,
isFromHY: true,
textLines: 1,
);
},
),
);
},
);
}
}
@@ -0,0 +1,188 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/alert/video/share_media_dialog.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/comment/comment_alert.dart';
import 'package:hgdj/hj_utils/api_service/common_service.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:like_button/like_button.dart';
///图集底部操作按钮组
class AlbumActionWidget extends StatefulWidget {
final VideoModel? videoModel;
final int currentIndex; //如果传-1,则不显示查看 '1/3'之类的数据
const AlbumActionWidget({super.key, this.videoModel, this.currentIndex = 0});
@override
State<AlbumActionWidget> createState() => _AlbumActionWidgetState();
}
class _AlbumActionWidgetState extends State<AlbumActionWidget> {
VideoModel? get model => widget.videoModel;
bool get showIndex => widget.currentIndex != -1;
@override
Widget build(BuildContext context) {
return Container(
height: 70,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: showIndex ? 9 : 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_likeBtn(),
_actionItem(
'count_msg.png'.communityPath,
model?.commentCount.countOr('评论') ?? '评论',
onTap: () => showCommentDialog(model?.id ?? ''),
),
_collectBtn(),
_actionItem(
'share_grey.png'.commonImgPath,
'分享',
onTap: () => Get.dialog(ShareMediaDialog(videoModel: model),
useSafeArea: false),
),
//页码 1/3
if (showIndex)
Container(
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 2),
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(20)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('${widget.currentIndex + 1}',
style: const TextStyle(
color: AppColors.actionRed, fontSize: 12)),
Text('/${model?.seriesCover?.length ?? 0}',
style: const TextStyle(
color: Color(0xff3D3D3D), fontSize: 12)),
],
),
),
],
),
);
}
//点赞
Widget _likeBtn() {
return LikeButton(
size: 20,
isLiked: model?.vidStatus?.hasLiked ?? false,
circleColor:
CircleColor(start: AppColors.actionRed, end: AppColors.actionRed),
bubblesColor: BubblesColor(
dotPrimaryColor: AppColors.actionRed,
dotSecondaryColor: AppColors.actionRed,
),
likeBuilder: (isLiked) => Image.asset(
isLiked
? 'like_red.png'.commonImgPath
: 'video_like_border.png'.videoPath,
width: 20,
height: 20,
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
),
likeCount: model?.likeCount ?? 0,
countBuilder: (count, isLiked, text) => Text(
count.countOr('点赞'),
style: TextStyle(
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
fontSize: 12),
),
onTap: (isLiked) async {
final hasLiked = model?.vidStatus?.hasLiked ?? false;
final count = model?.likeCount ?? 0;
bool isOk = false;
try {
isOk = hasLiked
? await CommonService.cancelLike(model?.id, 'PIC')
: await CommonService.sendLike(model?.id, 'PIC');
} catch (e) {
debugLog('album like $e');
}
//失败不动本地状态,返回原值让按钮回弹
if (!isOk) return hasLiked;
model?.likeCount = max(0, count + (hasLiked ? -1 : 1));
model?.vidStatus?.hasLiked = !hasLiked;
showToast(hasLiked ? '取消点赞' : '点赞成功');
if (mounted) setState(() {});
return !hasLiked;
},
);
}
//收藏
Widget _collectBtn() {
return LikeButton(
size: 20,
isLiked: model?.vidStatus?.hasCollected ?? false,
circleColor:
CircleColor(start: AppColors.actionRed, end: AppColors.actionRed),
bubblesColor: BubblesColor(
dotPrimaryColor: AppColors.actionRed,
dotSecondaryColor: AppColors.actionRed,
),
likeBuilder: (isLiked) => Image.asset(
isLiked
? 'collect_red.png'.commonImgPath
: 'collect_empty.png'.commonImgPath,
width: 20,
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
),
likeCount: model?.collectCount ?? 0,
countBuilder: (count, isLiked, text) => Text(
count.countOr('收藏'),
style: TextStyle(
color: isLiked ? AppColors.actionRed : const Color(0xff989898),
fontSize: 12),
),
onTap: (isLiked) async {
final hasCollected = model?.vidStatus?.hasCollected ?? false;
final count = model?.collectCount ?? 0;
final isOk =
await MineService.postCollect(model?.id, 'PIC', !hasCollected);
//失败不动本地状态,返回原值让按钮回弹
if (!isOk) return hasCollected;
model?.collectCount = max(0, count + (hasCollected ? -1 : 1));
model?.vidStatus?.hasCollected = !hasCollected;
showToast(hasCollected ? '已取消收藏' : '收藏成功');
if (mounted) setState(() {});
return !hasCollected;
},
);
}
//图标 + 文字按钮(评论、分享共用)
Widget _actionItem(String imagePath, String title,
{GestureTapCallback? onTap}) {
return InkWell(
enableFeedback: false,
onTap: onTap,
//Container 撑满 70 高,是点击热区,别删
child: Container(
alignment: Alignment.center,
child: Row(
children: [
Image.asset(imagePath, width: 20, color: const Color(0xff989898)),
3.sizeBoxW,
Text(title,
style: const TextStyle(color: Color(0xff989898), fontSize: 12)),
],
),
),
);
}
}
@@ -0,0 +1,70 @@
import 'dart:math';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/history_util.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/item_positions_listener.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/scrollable_positioned_list.dart';
import '../../../alert/video/video_buy_bottom_sheet.dart';
import '../../../alert/video/vip_acg_dialog.dart';
class PicsDetailLogic extends GetxController {
// ===== 外部传入 =====
final String? id;
PicsDetailLogic(this.id);
// ===== 数据 =====
VideoModel? videoModel;
// ===== Controller =====
// 两者都没有可调用的释放入口:ItemScrollController 只做 attach/detach
// ItemPositionsListener 的实现 ItemPositionsNotifier 内部虽持有 ValueNotifier,但包没暴露 dispose。所以本类无 onClose
final itemScrollCtr = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
// ===== 派生 getter =====
List<String> get covers => videoModel?.seriesCover ?? [];
//lockType 0-已解锁 1-金币解锁 2-会员解锁
bool get isUnlocked => videoModel?.lockType == 0;
bool get isCoinLock => videoModel?.lockType == 1;
//未解锁时放出几张:freeTime 为 0(后端没配)兜底 8 张,否则按 freeTime,都不超过总张数
//freeTime 为 null 走后半段算 0 张,与原逻辑一致
int get lockCount => videoModel?.freeTime == 0
? min(8, covers.length)
: min(videoModel?.freeTime ?? 0, covers.length);
@override
void onReady() {
super.onReady();
loadData();
}
Future loadData() async {
videoModel = await VidService.getDetail(id ?? '');
if (videoModel != null) {
HistoryUtil.insert(videoModel!, MediaStyle.Pic);
}
videoModel ??= VideoModel(); //拉失败也给个空壳,让页面落到错误态而不是一直转圈
update();
}
//解锁:金币走购买弹窗,会员走开通弹窗
void showBuy() async {
if (isCoinLock) {
final res =
await Get.bottomSheet(VideoBuyBottomSheet(video: videoModel!));
if (res == true) {
videoModel?.vidStatus?.hasPaid = true;
update();
}
} else if (videoModel?.lockType == 2) {
Get.dialog(VipACGDialog());
}
}
}
@@ -0,0 +1,200 @@
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/app_colors.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/hj_utils/widget_util.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/item_positions_listener.dart';
import 'package:hgdj/tools_base/widget/scroll_positioned_list/src/scrollable_positioned_list.dart';
import '../../mine/mine_share/mine_share_page.dart';
import 'album_action_widget.dart';
import 'pics_detail_logic.dart';
//图集详情页
class PicsDetailPage extends StatefulWidget {
final String? id;
const PicsDetailPage({super.key, this.id});
@override
State<PicsDetailPage> createState() => _PicsDetailPageState();
}
// 三个入口都是 Get.to(..., preventDuplicates: false),允许叠栈,用 uniqueTag 隔离各实例的 logic
class _PicsDetailPageState extends State<PicsDetailPage> with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GetBuilder<PicsDetailLogic>(
init: PicsDetailLogic(widget.id),
tag: uniqueTag,
builder: (logic) {
if (logic.videoModel == null) return LoadingCenterWidget();
//拉失败后 videoModel 是空壳,靠 id 判断
if (logic.videoModel?.id == null)
return CErrorWidget(retryOnTap: () => logic.loadData());
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//标题栏
Row(children: [
InkWell(
enableFeedback: false,
onTap: () => Get.back(),
child: Icon(Icons.arrow_back_ios_new,
color: Colors.white, size: 24),
),
4.sizeBoxW,
Expanded(
child: Text(
logic.videoModel?.title ?? '',
style: textStyle(16, Colors.white.withValues(alpha: .9),
FontWeight.w800),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
]).paddingSymmetric(horizontal: 16, vertical: 12),
Expanded(child: _picList(logic)),
//底部操作条(页码/评论/点赞/分享),未解锁传 -1 让它藏起页码
ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: logic.itemPositionsListener.itemPositions,
builder: (_, positions, __) => AlbumActionWidget(
videoModel: logic.videoModel,
currentIndex: logic.isUnlocked
? (positions.isEmpty ? 1 : positions.first.index)
: -1,
),
),
],
);
},
),
),
);
}
//图片列表:已解锁全量、点开看大图;未解锁只放 lockCount 张,最后一张压解锁蒙层
Widget _picList(PicsDetailLogic logic) {
final covers = logic.covers;
final isUnlocked = logic.isUnlocked;
final count = isUnlocked ? covers.length : logic.lockCount;
return ScrollablePositionedList.builder(
itemPositionsListener: logic.itemPositionsListener,
itemScrollController: logic.itemScrollCtr,
itemCount: count,
itemBuilder: (_, index) {
final image =
NetworkImageLoader(imageUrl: covers[index], borderRadius: 0);
if (isUnlocked) {
return GestureDetector(
onTap: () => ImageBrowserPage.open(covers, index: index),
child: image);
}
return Stack(
children: [
InkWell(
enableFeedback: false,
onTap: () => showToast('解锁后可查看图片详情'),
child: image),
if (index == count - 1) Positioned.fill(child: _unlockMask(logic)),
],
);
},
);
}
//解锁蒙层:高度由那张图决定,图太矮时整体等比缩小,别撑破
Widget _unlockMask(PicsDetailLogic logic) {
final coins = logic.videoModel?.originCoins ?? 0;
final List<Widget> content = logic.isCoinLock
? [
//金币解锁
EasyRichText(
'你只需花费 $coins 金币',
defaultStyle: textStyle(16, Colors.white, FontWeight.w400),
patternList: [
EasyRichTextPattern(
targetString: '$coins',
style: textStyle(16, AppColors.actionRed, FontWeight.w400)),
],
),
16.sizeBoxH,
Text('即可解锁继续查看',
style: textStyle(12, Colors.white, FontWeight.w400)),
16.sizeBoxH,
_maskBtn('立即解锁', 84, [Color(0xffFF6E6E), Color(0xffFF4D4D)],
Colors.white, logic.showBuy),
]
: [
//会员解锁
Text('试看结束', style: textStyle(16, Colors.white, FontWeight.w400)),
16.sizeBoxH,
Text('本内容需开通VIP免费观看',
style: textStyle(16, Colors.white, FontWeight.w400)),
16.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_maskBtn(
'邀请得3日VIP',
110,
[Color(0xffFFE8BE), Color(0xffE6B764)],
Color(0xff694923),
() => Get.to(() => MineSharePage())),
18.sizeBoxW,
_maskBtn(
'开通VIP免费看',
115,
[Color(0xffFF6E6E), Color(0xffFF4D4D)],
Colors.white,
logic.showBuy),
],
),
];
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: Container(
color: Colors.black.withValues(alpha: .6),
child: FittedBox(
fit: BoxFit.scaleDown,
child: SizedBox(
width: screen.screenWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: content),
),
),
),
),
);
}
//蒙层里的胶囊按钮,三处只差宽度/渐变/文字色/回调
Widget _maskBtn(String text, double width, List<Color> colors,
Color textColor, VoidCallback onTap) {
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)),
),
);
}
}
@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:hgdj/hj_model/cartoon_media_info.dart';
import 'package:hgdj/hj_page/find/rank_module/rank_badge.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/routers/jump_router.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
//动漫/漫画排行榜item
class ACGRankItem extends StatelessWidget {
final int index;
final CartoonMediaInfo? movie;
const ACGRankItem(this.index, this.movie, {super.key});
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: () => pushToCartoonPage(movie),
child: SizedBox(
height: 108,
child: Row(
children: [
//竖版封面
SizedBox(
width: 80,
height: 108,
child: NetworkImageLoader(
imageUrl: movie?.coverV ?? '',
borderRadius: 8,
),
),
10.sizeBoxW,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
rankBadge(index),
//标题
Text(
movie?.title ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 12,
height: 1.5,
),
),
//集数 · 更新状态
Text(
'${movie?.episodeNumberStatus ?? ''} · ${movie?.updateDesc ?? ''}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: _statusColor,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
),
);
}
//连载中金色,已完结半透明白
Color get _statusColor => movie?.updateStatus == 1
? const Color(0xffEEC76B)
: Colors.white.withValues(alpha: .45);
}
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/find/pic_collect/pics_detail_page.dart';
import 'package:hgdj/hj_page/find/rank_module/rank_badge.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
//图集排行榜item
class ImageRankItem extends StatelessWidget {
final int index;
final VideoModel? movie;
const ImageRankItem(this.index, this.movie, {super.key});
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: () =>
Get.to(PicsDetailPage(id: movie?.id), preventDuplicates: false),
child: SizedBox(
height: 108,
child: Row(
children: [
//封面
SizedBox(
width: 80,
height: 108,
child: NetworkImageLoader(
imageUrl: movie?.cover ?? '',
borderRadius: 8,
),
),
10.sizeBoxW,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
rankBadge(index),
//标题
Text(
movie?.title ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xE5FFFFFF),
fontSize: 12,
height: 1.5,
),
),
//浏览量 + 收藏量
Row(
children: [
Image.asset("eye_grey.webp".commonImgPath,
width: 14, height: 14),
5.sizeBoxW,
Text(
movie?.playCount?.countStr ?? "0",
style: const TextStyle(
color: Color(0xff989898), fontSize: 12),
),
8.sizeBoxW,
Image.asset("collect_start.webp".homePath, width: 14),
5.sizeBoxW,
Text(
movie?.collectCount?.countStr ?? "0",
style: const TextStyle(
color: Color(0xff989898), fontSize: 12),
),
],
),
],
),
),
],
),
),
);
}
}
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
/// 榜单名次徽标「皇冠 + NO.1」:影视 / 图集 / 动漫 / 帖子四种 item 共用,改样式只改这一处
Widget rankBadge(int index) {
final color = _rankColor(index);
return Row(
children: [
//4 个名次共用同一张皇冠形状图,只换颜色
Image.asset("rank_crown.webp".homePath, width: 24, color: color),
4.sizeBoxW,
Text("NO.${index + 1}", style: TextStyle(color: color, fontSize: 14)),
],
);
}
/// 名次配色:金 / 粉 / 蓝,第 4 名往后统一灰
Color _rankColor(int index) => switch (index) {
0 => const Color(0xffFFD059),
1 => const Color(0xffF24A71),
2 => const Color(0xff5DCAE7),
_ => const Color(0xffbdbdbd),
};
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/const.dart';
class RankMainLogic extends GetxController with GetTickerProviderStateMixin {
RankMainLogic({int initIndex = 0}) : _initIndex = initIndex;
final int _initIndex;
//榜单类型,顺序即 tab 顺序
static const tabs = [MediaStyle.Video, MediaStyle.Community, MediaStyle.Pic];
late final TabController tabCtr =
TabController(initialIndex: _initIndex, length: tabs.length, vsync: this);
@override
void onClose() {
tabCtr.dispose();
super.onClose();
}
}
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_page/find/rank_module/rank_type_tab_view.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import 'rank_main_logic.dart';
/// 排行榜主页:顶部 TabBar 切换榜单类型(影片/帖子/图集),下方 TabBarView 展示各类型榜单列表。
class RankMainPage extends StatefulWidget {
final int index; // 初始选中的 Tab 下标
const RankMainPage({super.key, this.index = 0});
@override
State<RankMainPage> createState() => _RankMainPageState();
}
class _RankMainPageState extends State<RankMainPage> with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return GetBuilder<RankMainLogic>(
init: RankMainLogic(initIndex: widget.index),
tag: uniqueTag,
builder: (logic) => Scaffold(
appBar: AppBar(title: const Text('热门排行')),
body: Column(
children: [
//顶部:榜单类型 Tab
Container(
height: 44,
alignment: Alignment.center,
child: TabBar(
controller: logic.tabCtr,
padding: const EdgeInsets.symmetric(horizontal: 30),
tabs: RankMainLogic.tabs
.map((e) => Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(e.rankTitle)))
.toList(),
labelPadding: const EdgeInsets.symmetric(horizontal: 3),
unselectedLabelColor: const Color(0x8CFFFFFF),
unselectedLabelStyle:
const TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
labelColor: const Color(0xE5FFFFFF),
labelStyle:
const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
indicator: CustomIndicator(
height: 3,
width: 13,
isGradient: true,
offsetY: 2,
borderRadius: BorderRadius.circular(1.5),
),
),
),
//下方:各榜单类型对应的列表(keepAlive 切换不重建)
Expanded(
child: TabBarView(
controller: logic.tabCtr,
children: RankMainLogic.tabs
.map((e) => RankTypeTabView(e).keepAlive)
.toList(),
),
),
],
),
),
);
}
}
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:hgdj/hj_utils/screen.dart';
/// 榜单周期 Tab:周榜 / 月榜 / 年榜 / 总榜,项间用一条竖线分隔、无下划线指示器。
/// 不用自己监听 controller 重绘——TabBar 内部就挂着 animation 监听
class RankSortTabBar extends StatelessWidget {
final List<String> titles;
final TabController? tabCtr;
const RankSortTabBar({super.key, required this.titles, this.tabCtr});
@override
Widget build(BuildContext context) {
return TabBar(
controller: tabCtr,
tabs: titles
.asMap()
.entries
.map((e) => Row(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 18, vertical: 3),
child: Text(e.value),
),
if (e.key != titles.length - 1) 12.lineV,
],
))
.toList(),
isScrollable: true,
labelPadding: EdgeInsets.zero,
labelStyle: const TextStyle(
fontSize: 14,
height: 1.5,
color: Color(0xE5FFFFFF),
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: const TextStyle(
fontSize: 14,
height: 1.5,
fontWeight: FontWeight.w400,
color: Color(0x8CFFFFFF),
),
indicator: const BoxDecoration(),
);
}
}
@@ -0,0 +1,43 @@
import 'package:hgdj/hj_utils/api_service/acg_service.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
import 'package:hgdj/tools_base/base_list_controller.dart';
class RankSubPageLogic extends ListBaseLogic<dynamic> {
final MediaStyle pageType; // 榜单媒体类型:影片/帖子/图集/漫画等
final int sortType; // 榜单周期:2周 3月 5年 4总
RankSubPageLogic({required this.pageType, this.sortType = 2});
bool get isACG =>
pageType == MediaStyle.Comics || pageType == MediaStyle.Cartoon;
// onInit(挂载即同步触发,比 onReady 的帧后回调在嵌套 keepAlive 下可靠)
@override
void onInit() {
super.onInit();
loadData();
}
// 下拉刷新 / 上拉加载;showLoading 用于重试时先清空显示 loading。
// 必须把 Future 交出去:CErrorWidget 靠它做重试 loading + 防连点
Future<void> loadData({bool isRefresh = true, bool showLoading = false}) {
if (showLoading) {
dataList = null;
update();
}
return fetchData(isRefresh: isRefresh, fetch: _fetch);
}
// 拉取榜单:ACG 走 getRanking,其余走 vidRanking;返回 (列表, 是否还有更多)
Future<(List<dynamic>?, bool)> _fetch(int page) async {
if (isACG) {
final resp = await ACGService.getRanking(page, 10,
mediaType: pageType.rankParam, type: sortType);
return (resp?.list, resp?.hasNext == true);
}
final res = await VidService.vidRanking(page, 10,
newsType: pageType.rankParam, type: sortType);
return (res?.videos, res?.hasNext == true);
}
}
@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/find/rank_module/video_rank_item.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/screen.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 '../../community/widget/community_post_widget.dart';
import 'acg_rank_item.dart';
import 'image_rank_item.dart';
import 'rank_sub_logic.dart';
class RankSubPage extends StatefulWidget {
final MediaStyle pageType;
final int sortType; //榜单周期:2周 3月 5年 4总
const RankSubPage({super.key, required this.pageType, this.sortType = 2});
@override
State<RankSubPage> createState() => _RankSubPageState();
}
// UniqueTagMixin:每次挂载都是全新 tag/controller,保证 onInit 触发、避免复用旧 logic
class _RankSubPageState extends State<RankSubPage> with UniqueTagMixin {
late final RankSubPageLogic logic = RankSubPageLogic(
pageType: widget.pageType,
sortType: widget.sortType,
);
@override
Widget build(BuildContext context) {
return GetBuilder<RankSubPageLogic>(
init: logic,
tag: uniqueTag,
builder: (_) => pullYsRefresh(
onInit: (ctr) => logic.refreshCtr = ctr,
onRefresh: (ctr) => logic.loadData(),
onLoading: (ctr) => logic.loadData(isRefresh: false),
child: _buildContent(),
),
);
}
Widget _buildContent() {
if (logic.isLoading) return LoadingCenterWidget();
//retryOnTap 要拿到 Future 才有 loading + 防连点
if (logic.isEmptyData)
return CErrorWidget(retryOnTap: () => logic.loadData(showLoading: true));
final list = logic.dataList ?? const [];
return ListView.separated(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: list.length,
separatorBuilder: (_, __) => 10.sizeBoxH,
itemBuilder: (_, index) => switch (logic.pageType) {
MediaStyle.Video => VideoRankItem(index, list[index]),
MediaStyle.Pic => ImageRankItem(index, list[index]),
//帖子要能左右滑浏览同榜其他帖,整表一起递进去(cast 是惰性视图,不拷贝)
MediaStyle.Community => CommunityPostWidget(
padding: EdgeInsets.zero,
videoModel: list[index],
videoModels: list.cast<VideoModel>(),
rankIndex: index,
),
MediaStyle.Cartoon ||
MediaStyle.Comics =>
ACGRankItem(index, list[index]),
_ => const SizedBox.shrink(),
},
);
}
}
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:hgdj/hj_page/find/rank_module/rank_sort_tab_bar.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import 'rank_sub_page.dart';
/// 某一类榜单(影片/帖子/图集/动漫…)下的周期分页:周榜 / 月榜 / 年榜 / 总榜
class RankTypeTabView extends StatefulWidget {
final MediaStyle pageType;
const RankTypeTabView(this.pageType, {super.key});
@override
State<RankTypeTabView> createState() => _RankTypeTabViewState();
}
class _RankTypeTabViewState extends State<RankTypeTabView>
with SingleTickerProviderStateMixin {
//标题与后端 sortType 绑在一起,避免两个数组分家后漏改一处就错位
static const _tabs = [
SortTab('周榜', 2),
SortTab('月榜', 3),
SortTab('年榜', 5),
SortTab('总榜', 4),
];
late final TabController tabCtr =
TabController(length: _tabs.length, vsync: this);
@override
void dispose() {
tabCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
RankSortTabBar(
titles: _tabs.map((e) => e.name).toList(), tabCtr: tabCtr),
8.sizeBoxH,
Expanded(
child: TabBarView(
controller: tabCtr,
children: _tabs
.map((e) =>
RankSubPage(pageType: widget.pageType, sortType: e.sort)
.keepAlive)
.toList(),
),
),
],
);
}
}
@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/find/rank_module/rank_badge.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/routers/jump_router.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
//影视排行榜item
class VideoRankItem extends StatelessWidget {
final int index;
final VideoModel? movie;
const VideoRankItem(this.index, this.movie, {super.key});
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: () => pushToVideoPage(videoModel: movie),
child: SizedBox(
height: 95,
child: Row(
children: [
//横版封面,左下播放量、右下时长
SizedBox(
width: 168,
height: 95,
child: Stack(
fit: StackFit.expand,
children: [
NetworkImageLoader(
imageUrl: movie?.cover ?? '', borderRadius: 8),
Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.fromLTRB(5, 0, 5, 5),
child: Row(
children: [
Image.asset('circle_play.webp'.videoPath, width: 16),
Text(
movie?.playCount?.countStr ?? '0',
style: const TextStyle(
color: Colors.white, fontSize: 10, height: 1.6),
),
const Spacer(),
Text(
movie?.playTime?.hmsStr ?? "",
style: const TextStyle(
color: Colors.white, fontSize: 10, height: 1.6),
),
],
),
),
],
),
),
10.sizeBoxW,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
rankBadge(index),
//末尾补个换行:单行标题也占满两行高,三行内容的间距才不会随标题长短跳
Text(
"${movie?.title ?? ''}\n",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xE5FFFFFF), fontSize: 12, height: 1.5),
),
//收藏量
Row(
children: [
Image.asset("collect_start.webp".homePath, width: 14),
5.sizeBoxW,
Text(
movie?.collectCount?.countStr ?? "0",
style: const TextStyle(
color: Color(0xff989898), fontSize: 12),
),
],
),
],
),
),
],
),
),
);
}
}