初始化
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
/// 小字筛选栏:文字 + 竖线分隔,用在标签/专题/搜索结果/短剧等列表页顶部
|
||||
class DividerTabBar extends StatelessWidget {
|
||||
final List<String> titles;
|
||||
final int selectIndex;
|
||||
final Function(int)? callback;
|
||||
final AlignmentGeometry? alignment;
|
||||
|
||||
const DividerTabBar(
|
||||
this.titles, {
|
||||
super.key,
|
||||
this.selectIndex = 0,
|
||||
this.callback,
|
||||
this.alignment,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: alignment ?? Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < titles.length; i++) _buildMenuButton(i),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuButton(int index) {
|
||||
final isLast = index == titles.length - 1;
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => callback?.call(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
titles[index],
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
color: index == selectIndex ? const Color(0xE5FFFFFF) : const Color(0x73FFFFFF),
|
||||
),
|
||||
),
|
||||
//分隔竖线跟在每项后面,最后一项不带
|
||||
if (!isLast)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
"|",
|
||||
style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: 0.05)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
|
||||
import '../../../hj_model/home/module_detail_model.dart';
|
||||
import '../../../hj_utils/sliver_delegate.dart';
|
||||
import '../home_sub_module/home_tab_section_logic.dart';
|
||||
import '../widget/home_sort_menu_view.dart';
|
||||
import 'video_simple_cell.dart';
|
||||
|
||||
/// 亚模块底部的「猜你喜欢」:标题 + 吸顶排序栏 + 两列网格
|
||||
class GuessLikeSliver extends StatelessWidget {
|
||||
final AllSection model;
|
||||
final int? sortIndex;
|
||||
final HomeTabSectionLogic? logic;
|
||||
|
||||
const GuessLikeSliver(this.model, {super.key, this.sortIndex, this.logic});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: Text(
|
||||
"猜你喜欢",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
delegate: MySliverDelegate(
|
||||
maxHeight: 42,
|
||||
minHeight: 42,
|
||||
childBuildHandler: (_, offset, overlaps, child) {
|
||||
return Container(
|
||||
color:
|
||||
(offset >= 0) ? AppColors.primaryColor : Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: HomeSortMenuView(
|
||||
["最多收藏", "最新上架", "最多观看"],
|
||||
gapChar: "|",
|
||||
selectIndex: sortIndex ?? 0,
|
||||
callback: (index) {
|
||||
if (index != sortIndex) {
|
||||
logic?.sortGuessLikeExchange(index);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (model.allVideoInfo == null)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: 300, child: LoadingCenterWidget()),
|
||||
)
|
||||
else if (model.allVideoInfo!.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: 300, child: CErrorWidget()),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(left: 12, right: 12),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 168 / 154,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) =>
|
||||
VideoSimpleCell(videoModel: model.allVideoInfo![index]),
|
||||
childCount: model.allVideoInfo!.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../hj_model/home/module_detail_model.dart';
|
||||
import '../../../hj_utils/api_service/vid_service.dart';
|
||||
import '../../../tools_base/debug_log.dart';
|
||||
import '../special_topic_detail/special_topics_detail_page.dart';
|
||||
import 'home_section_title.dart';
|
||||
import 'video_hor_cell.dart';
|
||||
import 'video_simple_cell.dart';
|
||||
|
||||
// OneLargeAndFourSmall = 101 or 0 // 0 一大四小
|
||||
// HScroll15 = 104 // 1 1.5左右滑动(横)
|
||||
// HScroll25 = 105 // 2 2.5左右滑动(横)
|
||||
// FourGrid = 102 // 3 四宫格(横acg+影视共用)
|
||||
// SixGrid = 103 // 4 六宫格(横)
|
||||
// HListGrid = 106 // 5 小列表 (横)
|
||||
// HListBigGrid = 107 // 6 大列表 (横)
|
||||
// VerticalScroll25 = 205 // 7 2.5左右滑动(竖ACG)
|
||||
// VerticalFourGrid = 201 // 8 四宫格(竖ACG)
|
||||
// VerticalSixGrid = 202 // 9 六宫格(竖ACG)
|
||||
// VerticalNineGrid = 203 // 10 九宫格(竖ACG)
|
||||
// VerticalListGrid = 204 // 11 小列表 (竖ACG)
|
||||
// GuessYouLike = 301 // 12 猜你喜欢
|
||||
enum HomeSectionShowType {
|
||||
oneLargeAndFourSmall(101, canExchange: true),
|
||||
hScroll15(104),
|
||||
hScroll25(105),
|
||||
fourGrid(102, canExchange: true),
|
||||
sixGrid(103, canExchange: true),
|
||||
hListGrid(106),
|
||||
hListBigGrid(107),
|
||||
verticalScroll25(205),
|
||||
verticalFourGrid(201),
|
||||
verticalSixGrid(202),
|
||||
verticalNineGrid(203),
|
||||
verticalListGrid(204),
|
||||
guessYouLike(301),
|
||||
;
|
||||
|
||||
final int value;
|
||||
final bool canExchange; //底部是否挂「更多片源 / 更换一批」
|
||||
|
||||
const HomeSectionShowType(this.value, {this.canExchange = false});
|
||||
}
|
||||
|
||||
//首页专题 section:按 showType 排版的内容 + (部分样式)底部换一批 + 间距分割线
|
||||
class HomeSectionCell extends StatefulWidget {
|
||||
final AllSection section;
|
||||
|
||||
const HomeSectionCell(this.section, {super.key});
|
||||
|
||||
@override
|
||||
State<HomeSectionCell> createState() => _HomeSectionCellState();
|
||||
}
|
||||
|
||||
class _HomeSectionCellState extends State<HomeSectionCell> {
|
||||
//value→枚举 O(1) 查找,避免每次 build 线性遍历 values
|
||||
static final Map<int?, HomeSectionShowType> _typeMap = {
|
||||
for (final t in HomeSectionShowType.values) t.value: t,
|
||||
};
|
||||
|
||||
AllSection get section => widget.section;
|
||||
|
||||
bool _exchanging = false; //换一批请求中
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//未知 showType 排版兜底一大四小,但不挂换一批(与原先按 101/102/103 判断保持一致)
|
||||
final style = _typeMap[section.showType];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_body(style),
|
||||
//101/102/103 底部挂「更多片源 / 更换一批」,其余样式只留间距
|
||||
if (style?.canExchange == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24), child: _exchangeBar())
|
||||
else
|
||||
12.sizeBoxH,
|
||||
//section 间的分割线跟着 cell 走,外面只管往列表里塞
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12, left: 16, right: 16),
|
||||
height: 1,
|
||||
color: const Color(0x19FFFFFF),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _body(HomeSectionShowType? style) {
|
||||
switch (style) {
|
||||
case HomeSectionShowType.hScroll15:
|
||||
return SectionOneHalfFiveHor(section);
|
||||
case HomeSectionShowType.hScroll25:
|
||||
return SectionTwoHalfFiveHV(section);
|
||||
case HomeSectionShowType.fourGrid:
|
||||
return SectionGridViewHV(section, maxCount: 4);
|
||||
case HomeSectionShowType.sixGrid:
|
||||
return SectionGridViewHV(section, maxCount: 6);
|
||||
case HomeSectionShowType.hListGrid:
|
||||
return SectionListView(section);
|
||||
case HomeSectionShowType.hListBigGrid:
|
||||
return SectionListView(section, isBigStyle: true);
|
||||
case HomeSectionShowType.verticalScroll25:
|
||||
return SectionTwoHalfFiveHV(section, isVertical: true);
|
||||
case HomeSectionShowType.verticalFourGrid:
|
||||
return SectionGridViewHV(
|
||||
section,
|
||||
maxCount: 4,
|
||||
aspectRatio: 168 / 266,
|
||||
textLines: 1,
|
||||
);
|
||||
case HomeSectionShowType.verticalSixGrid:
|
||||
return SectionGridViewHV(
|
||||
section,
|
||||
maxCount: 6,
|
||||
aspectRatio: 111 / 190,
|
||||
crossAxisCount: 3,
|
||||
textLines: 1,
|
||||
);
|
||||
case HomeSectionShowType.verticalNineGrid:
|
||||
return SectionGridViewHV(
|
||||
section,
|
||||
maxCount: 9,
|
||||
aspectRatio: 111 / 190,
|
||||
crossAxisCount: 3,
|
||||
textLines: 1,
|
||||
);
|
||||
case HomeSectionShowType.verticalListGrid:
|
||||
return SectionGridViewSmall(section);
|
||||
default:
|
||||
return SectionOneBigFourSmall(section);
|
||||
}
|
||||
}
|
||||
|
||||
//底部两颗胶囊:更多片源(跳专题详情) / 更换一批(换数据)
|
||||
Widget _exchangeBar() {
|
||||
return Container(
|
||||
height: 36,
|
||||
margin: const EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ExchangeButton(
|
||||
title: '更多片源',
|
||||
onTap: () => Get.to(SpecialTopicsDetailPage(section),
|
||||
preventDuplicates: false),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
Expanded(
|
||||
child: _ExchangeButton(
|
||||
title: '更换一批', loading: _exchanging, onTap: _exchange),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//换一批:只替换本 section 的视频(仅重建本 cell),失败保持原数据不动
|
||||
Future<void> _exchange() async {
|
||||
if (_exchanging) return;
|
||||
final id = section.sectionID;
|
||||
setState(() => _exchanging = true);
|
||||
try {
|
||||
final resp = await VidService.sectionVideoExchange(id);
|
||||
//请求飞行中列表可能被刷新,cell 按 index 复用后 section 已换人,结果不能再往回写
|
||||
if (resp.videos?.isNotEmpty == true && section.sectionID == id)
|
||||
section.allVideoInfo = resp.videos;
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
}
|
||||
if (mounted) setState(() => _exchanging = false);
|
||||
}
|
||||
}
|
||||
|
||||
//「更多片源 / 更换一批」胶囊按钮
|
||||
class _ExchangeButton extends StatelessWidget {
|
||||
final String title;
|
||||
final bool loading;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ExchangeButton(
|
||||
{required this.title, required this.onTap, this.loading = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0x1AFFFFFF),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
//转圈占位 16、常态 4,保持文字位置不跳
|
||||
if (loading)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CupertinoActivityIndicator(
|
||||
radius: 8, color: Colors.white))
|
||||
else
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//一大四小
|
||||
class SectionOneBigFourSmall extends StatelessWidget {
|
||||
final AllSection section;
|
||||
|
||||
const SectionOneBigFourSmall(this.section, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (section.allVideoInfo?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionTitle(section),
|
||||
Container(
|
||||
height: 235,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: VideoSimpleCell(
|
||||
videoModel: section.allVideoInfo!.first,
|
||||
textLines: 1,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
GridView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: 168 / 154,
|
||||
),
|
||||
itemCount: min(4, section.allVideoInfo!.length - 1),
|
||||
itemBuilder: (context, index) {
|
||||
VideoModel model = section.allVideoInfo![index + 1];
|
||||
return VideoSimpleCell(videoModel: model);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//1.5横
|
||||
class SectionOneHalfFiveHor extends StatelessWidget {
|
||||
final AllSection section;
|
||||
|
||||
const SectionOneHalfFiveHor(this.section, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (section.allVideoInfo?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionTitle(section),
|
||||
Container(
|
||||
height: 202,
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: section.allVideoInfo!.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
VideoModel model = section.allVideoInfo![index];
|
||||
return Container(
|
||||
height: 202,
|
||||
width: 280,
|
||||
margin: EdgeInsets.only(right: 8),
|
||||
child: VideoSimpleCell(videoModel: model, textLines: 1),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//2.5横/竖
|
||||
class SectionTwoHalfFiveHV extends StatelessWidget {
|
||||
final AllSection section;
|
||||
final bool isVertical; // true 竖, false 横
|
||||
const SectionTwoHalfFiveHV(this.section,
|
||||
{super.key, this.isVertical = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (section.allVideoInfo?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionTitle(section),
|
||||
Container(
|
||||
height: isVertical ? 218 : 146,
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final width = (constraints.maxWidth - 2 * 8) / 2.5;
|
||||
return SizedBox(
|
||||
height: isVertical ? 218 : 146,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: section.allVideoInfo!.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
VideoModel model = section.allVideoInfo![index];
|
||||
return Container(
|
||||
height: 146,
|
||||
width: width,
|
||||
margin: EdgeInsets.only(right: 8),
|
||||
child: VideoSimpleCell(
|
||||
videoModel: model,
|
||||
textLines: isVertical ? 1 : 2,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//四/六/九宫格横/竖 (默认四宫格)
|
||||
class SectionGridViewHV extends StatelessWidget {
|
||||
final AllSection section;
|
||||
final int maxCount;
|
||||
final double aspectRatio;
|
||||
final int crossAxisCount;
|
||||
final int textLines;
|
||||
|
||||
const SectionGridViewHV(
|
||||
this.section, {
|
||||
super.key,
|
||||
this.maxCount = 4,
|
||||
this.aspectRatio = 168 / 154,
|
||||
this.crossAxisCount = 2,
|
||||
this.textLines = 2,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (section.allVideoInfo?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionTitle(section),
|
||||
GridView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: aspectRatio,
|
||||
),
|
||||
itemCount: min(maxCount, section.allVideoInfo!.length),
|
||||
itemBuilder: (context, index) {
|
||||
VideoModel model = section.allVideoInfo![index];
|
||||
return VideoSimpleCell(videoModel: model, textLines: textLines);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//列表横(大小横)
|
||||
class SectionListView extends StatelessWidget {
|
||||
final AllSection section;
|
||||
final bool isBigStyle; // true 大横, false 小横
|
||||
const SectionListView(this.section, {super.key, this.isBigStyle = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (section.allVideoInfo?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionTitle(section),
|
||||
ListView.separated(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
shrinkWrap: true,
|
||||
itemCount: min(3, section.allVideoInfo!.length),
|
||||
separatorBuilder: (context, index) => 12.sizeBoxH,
|
||||
itemBuilder: (context, index) {
|
||||
VideoModel model = section.allVideoInfo![index];
|
||||
if (isBigStyle) {
|
||||
return SizedBox(
|
||||
height: 230,
|
||||
child: VideoSimpleCell(videoModel: model, textLines: 1),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 96,
|
||||
child: VideoHorCell(videoModel: model),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//小列表(竖)
|
||||
class SectionGridViewSmall extends StatelessWidget {
|
||||
final AllSection section;
|
||||
|
||||
const SectionGridViewSmall(this.section, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (section.allVideoInfo?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HomeSectionTitle(section),
|
||||
Container(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
height: 232,
|
||||
child: GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
scrollDirection: Axis.horizontal,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 110 / 256,
|
||||
),
|
||||
itemCount: section.allVideoInfo!.length,
|
||||
itemBuilder: (context, index) {
|
||||
VideoModel model = section.allVideoInfo![index];
|
||||
return VideoHorCell(
|
||||
videoModel: model,
|
||||
imageWidth: 82,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../hj_model/home/module_detail_model.dart';
|
||||
import '../special_topic_detail/special_topics_detail_page.dart';
|
||||
|
||||
/// 专题标题行:标题 + 右箭头,点整行进专题详情
|
||||
class HomeSectionTitle extends StatelessWidget {
|
||||
final AllSection? section;
|
||||
|
||||
const HomeSectionTitle(this.section, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
Get.to(SpecialTopicsDetailPage(section), preventDuplicates: false),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
section?.sectionName ?? "",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Image.asset("arrow_right_grey.webp".commonImgPath, width: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../config/config.dart';
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../hj_utils/free_play_manager.dart';
|
||||
|
||||
/// 封面右上角的售卖角标:免费试看 / 金币 / VIP,三者互斥,都不满足就不占位。
|
||||
/// 展不展示还受 ping 下发的 freeMark / coinMark / vipMark 三个开关控制
|
||||
class LevelMarkChip extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
|
||||
const LevelMarkChip(this.videoModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (videoModel?.freeArea == true) return const SizedBox();
|
||||
// 免费试看:ping 免费类角标总开关 freeMark + 实时试看权益(次数用尽即不再展示)
|
||||
if (Config.freeMark && FreePlayManager().canShowFreeTrialBadge(videoModel)) {
|
||||
return _chip("免费试看", const Color(0xff141414), const [Color(0xff35DEBC), Color(0xff22BB9C)]);
|
||||
}
|
||||
final isCoin = videoModel?.isCoinVideo() == true;
|
||||
if (isCoin && Config.coinMark) {
|
||||
return _chip("金币", const Color(0xff8B3E00), const [Color(0xffFFE580), Color(0xffFACC15)]);
|
||||
}
|
||||
if (!isCoin && Config.vipMark) {
|
||||
return _chip("VIP", const Color(0xff141414), const [Color(0xffF8F3AE), Color(0xffDDAC44)]);
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
Widget _chip(String text, Color textColor, List<Color> gradientColors) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
gradient: LinearGradient(colors: gradientColors),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: textColor, fontSize: 10, height: 1.6),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/config/config.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';
|
||||
|
||||
import '../../../hj_model/home/plate_model.dart';
|
||||
import '../../../hj_model/splash/domain_source_model.dart';
|
||||
|
||||
/// 金刚区:亚模块顶部那排图标入口,横向可滑,一屏露 5.5 个。
|
||||
/// 配置来自 /domain 下发的 jgArea,按 mid == 亚模块 id 匹配;没配就整块不占位
|
||||
class QuickEntryRow extends StatelessWidget {
|
||||
final ModuleData? tabModel;
|
||||
const QuickEntryRow(this.tabModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dataArr =
|
||||
(Config.jgArea ?? []).where((e) => e.mid == tabModel?.id).toList();
|
||||
if (dataArr.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
height: 62,
|
||||
margin: const EdgeInsets.fromLTRB(12, 0, 0, 12),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = (constraints.maxWidth - 5 * 16) / 5.5;
|
||||
final height = width * 48 / 58;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: ListView.builder(
|
||||
itemCount: dataArr.length,
|
||||
padding: EdgeInsets.zero,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) =>
|
||||
_buildItem(dataArr[index], width),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(JGAreaModel model, double width) {
|
||||
return Container(
|
||||
height: 62,
|
||||
width: width,
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => pushToPageByLink(model.linkUrl),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: NetworkImageLoader(imageUrl: model.img ?? ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
3.sizeBoxH,
|
||||
Text(
|
||||
model.name ?? "",
|
||||
maxLines: 1,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
/// 抖音样式的视频 item:封面 + 底部角标 + 标题 +(可选)标签/评论数。
|
||||
/// 封面占满父级剩余高度,外层必须给约束(网格 / 固定高容器)
|
||||
class TiktokSimpleCell extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
final int textLines;
|
||||
final bool isShowBottom; // true 显示底部标签和评论数
|
||||
final bool isShowTime; // true 封面右下角显示时长,false 显示图集数
|
||||
|
||||
const TiktokSimpleCell({
|
||||
super.key,
|
||||
this.videoModel,
|
||||
this.textLines = 2,
|
||||
this.isShowTime = false,
|
||||
this.isShowBottom = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(child: _buildCover()),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
//末尾补个换行,让不满两行的标题也占满高度,卡片不会参差
|
||||
child: Text(
|
||||
"${videoModel?.title ?? ""}\n",
|
||||
maxLines: textLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9), fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (isShowBottom) _buildBottom(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (videoModel?.isRandomAd() == true) {
|
||||
pushToPageByLink(videoModel?.randomAdsInfo?.href);
|
||||
return;
|
||||
}
|
||||
pushToVideoPage(videoModel: videoModel);
|
||||
}
|
||||
|
||||
//封面:图 + 底部渐变角标条
|
||||
Widget _buildCover() {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(imageUrl: videoModel?.cover ?? "", borderRadius: 4),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(bottom: Radius.circular(4)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0),
|
||||
Colors.black.withValues(alpha: 0.6)
|
||||
],
|
||||
),
|
||||
),
|
||||
child: _buildCoverBadge(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//角标条:左播放数,右边给时长或图集数
|
||||
Widget _buildCoverBadge() {
|
||||
return Row(
|
||||
children: [
|
||||
Image.asset("eye_white.webp".commonImgPath, width: 16),
|
||||
3.sizeBoxW,
|
||||
Text(
|
||||
videoModel?.playCount?.countStr ?? "0",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: Colors.white, fontSize: 10, height: 1.6),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isShowTime)
|
||||
Text(
|
||||
videoModel?.playTime?.hmsStr ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10),
|
||||
)
|
||||
else ...[
|
||||
Image.asset("tuji_icon.webp".communityPath, width: 16),
|
||||
2.sizeBoxW,
|
||||
Text(
|
||||
'${videoModel?.seriesCover?.length ?? 0}',
|
||||
textAlign: TextAlign.justify,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10),
|
||||
),
|
||||
],
|
||||
2.sizeBoxW,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//标签 + 评论数
|
||||
Widget _buildBottom() {
|
||||
final tagName = videoModel?.tags?.firstOrNull?.name ?? "";
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
//标签可能是很长的脏数据(id),Flexible 才能让 ellipsis 生效,否则会撑爆 Row
|
||||
Flexible(
|
||||
child: tagName.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
tagName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0x73FFFFFF), fontSize: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
"评论${videoModel?.commentCount?.countStr ?? 0}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Color(0x73FFFFFF), fontSize: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../hj_utils/free_play_manager.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'level_mark_chip.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
/// 横版视频 item:左封面(带售卖角标) + 右标题/收藏数,用在专题的小列表样式
|
||||
class VideoHorCell extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
final double imageWidth;
|
||||
|
||||
const VideoHorCell({super.key, this.videoModel, this.imageWidth = 150});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
width: imageWidth,
|
||||
height: double.infinity,
|
||||
borderRadius: 4,
|
||||
imageUrl: videoModel?.cover ?? "",
|
||||
),
|
||||
Positioned(
|
||||
right: 6,
|
||||
top: 6,
|
||||
// 监听免费次数变化:会话内试看用尽时角标实时刷新,不再残留「免费试看」
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: FreePlayManager().revision,
|
||||
builder: (_, __, ___) => LevelMarkChip(videoModel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${videoModel?.title ?? ""}\n",
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
"收藏:${videoModel?.collectCount?.countStr ?? "0"}",
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: Color(0x73FFFFFF),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (videoModel?.isRandomAd() == true) {
|
||||
pushToPageByLink(videoModel?.randomAdsInfo?.href);
|
||||
return;
|
||||
}
|
||||
pushToVideoPage(videoModel: videoModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_item.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
import '../../../hj_model/splash/ads_model.dart';
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../hj_utils/free_play_manager.dart';
|
||||
import '../../../tools_base/banner/ads_banner_widget.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'level_mark_chip.dart';
|
||||
|
||||
/// 竖版视频 item:封面(角标/售卖标/暗网遮罩) + 标题 +(可选)标签/评论数。
|
||||
/// 命中广告数据时整体换成 [VideoSimpleAdsCell]
|
||||
class VideoSimpleCell extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
final BorderRadius? imgBorderRadius;
|
||||
final GestureTapCallback? onTap;
|
||||
final int textLines;
|
||||
final bool isFromHY; // 黄油风格
|
||||
final bool isShowBottom; // true 显示底部标签和评论数
|
||||
final bool isFromSearch;
|
||||
final double titleFontSize; // 标题字号,小卡场景传小值
|
||||
final double coverFontSize; // 封面底部播放量/时长字号,小卡场景传小值避免溢出
|
||||
final String? coverRightText; // 封面右下角文案,默认时长;短剧传「共N集」
|
||||
final bool showLevelIcon; // 封面右上角「金币/VIP/免费试看」角标;短剧按集卖,整部剧标一个对不上,传 false
|
||||
final ValueChanged<TagsBean>? onTagTap; // 左下角标签点击;短剧跳标签详情
|
||||
|
||||
const VideoSimpleCell({
|
||||
super.key,
|
||||
this.videoModel,
|
||||
this.imgBorderRadius,
|
||||
this.textLines = 2,
|
||||
this.onTap,
|
||||
this.isFromHY = false,
|
||||
this.isShowBottom = true,
|
||||
this.isFromSearch = false,
|
||||
this.titleFontSize = 12,
|
||||
this.coverFontSize = 10,
|
||||
this.coverRightText,
|
||||
this.showLevelIcon = true,
|
||||
this.onTagTap,
|
||||
});
|
||||
|
||||
// 暗网风格遮罩:搜索来源且命中暗标签
|
||||
bool get isDarkStyle => isFromSearch && (videoModel?.isDarkTag ?? false);
|
||||
|
||||
// 暗网内容且非 VIP,需引导开通才能观看
|
||||
bool get _needVipUnlock => isDarkStyle && !globalStore.isAWVIP;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap ?? _onTap,
|
||||
child: videoModel?.isAdsArr() == true
|
||||
? VideoSimpleAdsCell(
|
||||
videoModel: videoModel,
|
||||
textLines: textLines,
|
||||
)
|
||||
: _buildContent(),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (videoModel?.isRandomAd() == true) {
|
||||
pushToPageByLink(videoModel?.randomAdsInfo?.href);
|
||||
return;
|
||||
}
|
||||
if (_needVipUnlock) {
|
||||
pushToWalletPage(vipId: Config.darkWebVipId);
|
||||
return;
|
||||
}
|
||||
pushToVideoPage(videoModel: videoModel);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(child: _buildCoverItem()),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
//末尾补个换行,让不满两行的标题也占满高度,卡片不会参差
|
||||
child: Text(
|
||||
"${videoModel?.title ?? ""}\n",
|
||||
maxLines: textLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: titleFontSize),
|
||||
),
|
||||
),
|
||||
if (isShowBottom) _buildBottomWidget(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverItem() {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: videoModel?.cover ?? "",
|
||||
imgBorderRadius: imgBorderRadius,
|
||||
borderRadius: 4,
|
||||
blur: _needVipUnlock,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 24,
|
||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(4)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0),
|
||||
Colors.black.withValues(alpha: 0.6),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: isFromHY ? _buildCoverHYBottom() : _buildCoverNormalBottom(),
|
||||
),
|
||||
),
|
||||
if (showLevelIcon)
|
||||
Positioned(
|
||||
right: 6,
|
||||
top: 6,
|
||||
// 监听免费次数变化:会话内试看用尽时角标实时刷新,不再残留「免费试看」
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: FreePlayManager().revision,
|
||||
builder: (_, __, ___) => LevelMarkChip(videoModel),
|
||||
),
|
||||
),
|
||||
if (_needVipUnlock)
|
||||
Container(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: EasyRichText(
|
||||
'需要开通 ${Config.darkWebVipName}\n即可观看',
|
||||
textAlign: TextAlign.center,
|
||||
defaultStyle: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
height: 1.6,
|
||||
),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: Config.darkWebVipName,
|
||||
style: TextStyle(color: Color(0xffF68804)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverNormalBottom() {
|
||||
return Row(
|
||||
children: [
|
||||
Image.asset('circle_play.webp'.videoPath, width: 16),
|
||||
1.sizeBoxW,
|
||||
Text(
|
||||
videoModel?.playCount?.countStr ?? "0",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: coverFontSize, height: 1.6),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
coverRightText ?? videoModel?.playTime?.hmsStr ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: coverFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverHYBottom() {
|
||||
return Row(
|
||||
children: [
|
||||
Image.asset("eye_white.webp".commonImgPath, width: 16),
|
||||
1.sizeBoxW,
|
||||
Text(
|
||||
videoModel?.pageViewCount?.countStr ?? "0",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white, fontSize: 10, height: 1.6),
|
||||
),
|
||||
Spacer(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomWidget() {
|
||||
if (isFromHY) {
|
||||
final tagDesc =
|
||||
videoModel?.tags?.map((e) => e.name ?? "").join("·") ?? "";
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 3),
|
||||
child: Text(
|
||||
tagDesc,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0x73FFFFFF),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
//标签可能是很长的脏数据(id),Flexible 才能让 ellipsis 生效,否则会撑爆 Row
|
||||
Flexible(
|
||||
child: videoModel?.tags?.isNotEmpty == true
|
||||
? GestureDetector(
|
||||
onTap: onTagTap == null
|
||||
? null
|
||||
: () => onTagTap!(videoModel!.tags!.first),
|
||||
child: Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
videoModel?.tags?.first.name ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0x73FFFFFF),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
"评论${videoModel?.commentCount?.countStr ?? 0}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0x73FFFFFF),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//视频穿插广告
|
||||
class VideoSimpleAdsCell extends StatefulWidget {
|
||||
final VideoModel? videoModel;
|
||||
final int textLines;
|
||||
|
||||
const VideoSimpleAdsCell({
|
||||
super.key,
|
||||
this.videoModel,
|
||||
this.textLines = 2,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoSimpleAdsCell> createState() => _VideoSimpleAdsCellState();
|
||||
}
|
||||
|
||||
class _VideoSimpleAdsCellState extends State<VideoSimpleAdsCell> {
|
||||
VideoModel? get videoModel => widget.videoModel;
|
||||
|
||||
int adIndex = 0;
|
||||
|
||||
List<AdsInfoModel>? get adsInfoArr =>
|
||||
videoModel?.adsInfoArr; //?? mediaInfo?.adsInfoArr;
|
||||
|
||||
AdsInfoModel? get adsModel => videoModel?.randomAdsInfo;
|
||||
|
||||
// 广告点击:跳落地页
|
||||
void _onAdTap() {
|
||||
final ad = adsInfoArr![adIndex];
|
||||
pushToPageByLink(ad.href);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (adsModel != null) {
|
||||
return _buildSimpleAD(adsModel);
|
||||
} else if (adsInfoArr?.isNotEmpty == true) {
|
||||
return _buildMuliAds();
|
||||
} else {
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMuliAds() {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _onAdTap,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
// isHor true: 大横屏广告
|
||||
bool isHor = (screen.screenWidth - 36) <= constraints.maxWidth;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: AdsBannerWidget(
|
||||
adsInfoArr,
|
||||
autoPlayMs: 3000,
|
||||
borderRadius: 4,
|
||||
onIndexChanged: (index) {
|
||||
adIndex = index;
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: 4),
|
||||
// 广告轮播切到下一条时,标题做渐入渐出切换
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
layoutBuilder: (current, previous) => Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [...previous, if (current != null) current],
|
||||
),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: Text(
|
||||
isHor
|
||||
? adsInfoArr![adIndex].title?.trim() ?? ""
|
||||
: "${adsInfoArr![adIndex].title?.trim() ?? ""}\n",
|
||||
key: ValueKey(adIndex), // adIndex 变化触发切换动画
|
||||
maxLines: widget.textLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isHor)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Text(
|
||||
"",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSimpleAD(AdsInfoModel? adModel) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _onAdTap,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
// isHor true: 大横屏广告
|
||||
bool isHor = (screen.screenWidth - 32) <= constraints.maxWidth;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AdsItem(
|
||||
adInfo: adModel!,
|
||||
borderRadius: 4,
|
||||
showType: AdShowType.img,
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 16,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [
|
||||
Color.fromRGBO(255, 235, 58, 1),
|
||||
Color.fromRGBO(255, 235, 58, 1),
|
||||
]),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
)),
|
||||
child: Text(
|
||||
"广告",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color.fromRGBO(0, 0, 0, 1),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
isHor
|
||||
? videoModel?.title ?? ""
|
||||
: "${videoModel?.title ?? ""}\n",
|
||||
maxLines: widget.textLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isHor)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Text(
|
||||
"",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user