初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
/// 合集(专辑)信息,会被 history_util 序列化进本地观看历史
class CollectionModel {
int? collectCount;
int? commentCount;
String? cover;
String? createdAt;
bool? hasBought;
String? id;
bool? isRecommend;
int? likeCount;
int? mediaCount;
String? name;
int? price;
String? updateTime;
int? viewCount;
CollectionModel.fromJson(Map<String, dynamic> json) {
collectCount = json['collectCount'];
commentCount = json['commentCount'];
cover = json['cover'];
createdAt = json['createdAt'];
hasBought = json['hasBought'];
id = json['id'];
isRecommend = json['isRecommend'];
likeCount = json['likeCount'];
mediaCount = json['mediaCount'];
name = json['name'];
price = json['price'];
updateTime = json['updateTime'];
viewCount = json['viewCount'];
}
Map<String, dynamic> toJson() => {
'collectCount': collectCount,
'commentCount': commentCount,
'cover': cover,
'createdAt': createdAt,
'hasBought': hasBought,
'id': id,
'isRecommend': isRecommend,
'likeCount': likeCount,
'mediaCount': mediaCount,
'name': name,
'price': price,
'updateTime': updateTime,
'viewCount': viewCount,
};
}
+140
View File
@@ -0,0 +1,140 @@
import '../actress_info.dart';
import '../cartoon_media_info.dart';
import '../splash/ads_model.dart';
import '../video_model.dart';
/// 模块详情:一个首页 tab 拉回来的全部内容
class ModuleDetailModel {
List<AllSection>? allSection; //所有专题
List<VideoModel>? allVideoInfo; //所有视频
List<ActressInfo>? allActress;
List<CartoonMediaInfo>? allMediaInfo;
List<VideoModel>? chosenVideoInfo; // 精选视频
bool? hasNext;
//只有热门随机刷新那条路径会手工造一个,只带 allVideoInfo
ModuleDetailModel({this.allVideoInfo});
ModuleDetailModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
chosenVideoInfo = _list(json['chosenVideoInfo'], (e) => VideoModel.fromJson(e));
allSection = _list(json['allSection'], (e) => AllSection.fromJson(e));
allVideoInfo = _list(json['allVideoInfo'], (e) => VideoModel.fromJson(e));
allActress = _list(json['allActress'], (e) => ActressInfo.fromJson(e));
allMediaInfo = _list(json['allMediaInfo'], (e) => CartoonMediaInfo.fromJson(e));
hasNext = json['hasNext'];
}
//某个字段下发成非数组时只丢这一项,别让整个模块的解析失败
static List<T>? _list<T>(dynamic v, T Function(dynamic) fromJson) => v is List ? v.map(fromJson).toList() : null;
}
/// 专题:既是首页楼层,也被 AdManager 拿来当广告占位插进列表
class AllSection {
String? sectionID;
String? sectionName;
String? sectionTitle;
String? sectionDesc;
String? sectionCover;
String? desc;
List<TagsBean>? allTags;
List<VideoModel>? allVideoInfo;
List<CartoonMediaInfo>? allMediaInfo;
List<ActressInfo>? allActress;
int? linkType;
String? linkUrl;
/// 横版(长视频):101 一大四小 / 102 四宫格 / 103 六宫格 / 104 1.5滑动 / 105 2.5滑动 / 106 横屏列表 / 107 横屏大图
/// 竖版(短视频、ACG)201 四宫格 / 202 六宫格 / 203 九宫格 / 204 1.5滑动 / 205 2.5滑动
/// 301 猜你喜欢
int? showType;
int? type;
int? voteCount; //大胸投票
int? disVoteCount; //贫胸投票
int? collCount;
int? viewCounts;
bool? collected;
bool? hasNext;
bool? hot;
///videoModel是否随机广告
AdsInfoModel? randomAdsInfo;
List<AdsInfoModel>? adsInfoArr;
bool get isGuessLike => showType == 301; // 猜你喜欢
bool isRandomAd() {
return randomAdsInfo != null;
}
bool isAdsArr() {
return adsInfoArr != null;
}
AllSection();
AllSection.fromJson(dynamic json) {
allActress = (json['allActress'] as List?)?.map((e) => ActressInfo.fromJson(e)).toList();
allVideoInfo = (json['allVideoInfo'] as List?)?.map((e) => VideoModel.fromJson(e)).toList();
allMediaInfo = (json['allMediaInfo'] as List?)?.map((e) => CartoonMediaInfo.fromJson(e)).toList();
allTags = (json['allTags'] as List?)?.map((e) => TagsBean.fromMap(e)).toList();
hot = json['hot'];
desc = json['desc'];
sectionDesc = json['sectionDesc'];
collCount = json['collCount'];
viewCounts = json['viewCounts'];
linkType = json['linkType'];
linkUrl = json['linkUrl'];
sectionID = json['sectionID'] ?? json['id'];
sectionName = json['sectionName'];
showType = json['showType'];
type = json['type'];
voteCount = json['voteCount'];
disVoteCount = json['disVoteCount'];
collected = json['collected'];
sectionCover = json['sectionCover'];
sectionTitle = json['sectionTitle'];
hasNext = json['hasNext'];
}
}
/// 片库筛选用的标签,会随 Keyword 一起回传给接口
class Tags {
String? coverImg;
String? description;
bool? hasCollected;
String? id;
String? name;
int? playCount;
Tags.fromJson(Map<String, dynamic> json) {
coverImg = json['coverImg'];
description = json['description'];
hasCollected = json['hasCollected'];
id = json['id'];
name = json['name'];
playCount = json['playCount'];
}
Map<String, dynamic> toJson() => {
'coverImg': coverImg,
'description': description,
'hasCollected': hasCollected,
'id': id,
'name': name,
'playCount': playCount,
}..removeWhere((key, value) => value == null);
}
/// 福利页的限免专区,只用 id 去拉列表、title 做标题
class WareDiscountAreaData {
String? id;
String? title;
WareDiscountAreaData.fromJson(Map<String, dynamic> json) {
id = json["id"];
title = json["title"];
}
}
+139
View File
@@ -0,0 +1,139 @@
import 'package:collection/collection.dart';
import 'package:hgdj/hj_utils/const.dart';
//亚模块模型
class ModuleData {
String? id;
String? moduleName;
//1首页(原首页视频) 2社区 3暗网 4动漫 5漫画 6小说 7黄油 8图集 9短视频 10私密圈 11AI广场 12短剧
int? type;
//1 模块普通海角系样式(HJShowType) 2 全专题组合样式(AllSectionShowType)
int? showType;
bool? pureVersion; //true:广告纯净模式
String? defaultTagId; //社区默认选中
HaiJiaoStyle? haiJiaoStyle;
ModuleData({this.moduleName, this.type, this.id, this.showType});
ModuleData.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
moduleName = json['moduleName'];
type = json['type'];
showType = json['showType'];
pureVersion = json['pureVersion'];
defaultTagId = json['defaultTagId'];
haiJiaoStyle = HaiJiaoStyle.fromJson(json['haiJiaoStyle']);
}
//只给 ModuleSortManager 存本地排序用,读回来只按 id 跟后端最新数据配对,故不存 haiJiaoStyle 等内容字段
Map<String, dynamic> toJson() {
return {
"id": id,
"moduleName": moduleName,
"type": type,
"showType": showType,
'defaultTagId': defaultTagId,
};
}
//动漫/漫画/小说
bool get isACG => type == 4 || type == 5 || type == 6;
/// 展示用的排序项:top=true 的置顶,最多 4 个。
/// 标题、sort 值、随机刷新配置一律从这份派生——分头各排一次,遇到同名/同值 remove 会删错元素,两边就对不上了。
/// haiJiaoStyle 解析完就不再变,算一次存起来,别每个 getter 各构造一遍
late final List<SortRuleModel> _sortedRules = () {
final rules = [...?haiJiaoStyle?.sortRules];
final topIndex = rules.indexWhere((e) => e.top == true);
if (topIndex > 0) rules.insert(0, rules.removeAt(topIndex));
return rules.length > 4 ? rules.sublist(0, 4) : rules;
}();
/// 排序 tab(标题 + sort 值),为空返回 null 让调用方退回默认值。
/// 标题和 sort 值必须一起产出:拆成两个并行数组时,后端只下发其中一个就会静默错位
List<SortTab<int>>? get sortTabs => _sortedRules.isEmpty
? null
: _sortedRules.map((e) => SortTab(e.name ?? '', e.val ?? -1)).toList();
/// 按 sort 值取对应的排序规则(用于判断该排序项是否走随机刷新接口)
SortRuleModel? sortRuleOf(int? val) =>
_sortedRules.firstWhereOrNull((e) => e.val == val);
}
class HaiJiaoStyle {
int? defaultShow; //默认展示 0-一排两个 1-一排一个
int? sectionStyle; //专题展示样式 0-不展示 1-十六岁专题 2-女优专题 3-网黄专题 4-普通专题
int? showChosenVideo; //0-不展示精选视频 1-展示精选视频
int? sortShow; //排序规则展示样式 0-不展示 1-展示
List<SortRuleModel>? sortRules; //排序规则
HaiJiaoStyle.fromJson(Map<String, dynamic>? json) {
json ??= {};
defaultShow = json['defaultShow'];
sectionStyle = json['sectionStyle'];
showChosenVideo = json['showChosenVideo'];
sortShow = json['sortShow'];
final rules = json['sortRules'];
if (rules is List)
sortRules = rules.map((e) => SortRuleModel.fromJson(e)).toList();
}
}
class SortRuleModel {
int? val;
bool? top; //是否置顶
String? name;
//刷新模式 DEFAULT / RANDOM_TOP_N,判断随机刷新只认这个字段,不许匹配 name 或硬编码 val
String? refreshMode;
SortRuleModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
val = json['val'];
top = json['top'];
name = json['name'];
refreshMode = json['refreshMode'];
}
bool get isRandomRefresh => refreshMode == 'RANDOM_TOP_N';
}
class HomePlateModel {
//解析一律走 _modules,字段缺失也退化成空列表,所以都是非空的,调用方不用判空
List<ModuleData> homePage = []; //首页-视频
List<ModuleData> deepWeb = []; //暗网
List<ModuleData> community = []; //社区
List<ModuleData> novel = []; //小说
List<ModuleData> pics = []; //图集:后端原始模块,展示要用 picsUi
List<ModuleData> game = []; //黄游:后端原始模块,展示要用 gameUi
List<ModuleData> dramaPage = []; //短剧专题(与 update-markers.modules 按 id 对齐)
//图集:固定注入「热门推荐」(type -100 对应 PIC 数据源) 再接后端模块
List<ModuleData> get picsUi => [
ModuleData(moduleName: '热门推荐', type: -100, showType: 1),
...pics,
];
//黄游:固定注入「热门推荐」(type -101 对应 SEED_LINK 数据源) 再接后端模块,showType 2 走列表样式
List<ModuleData> get gameUi => [
ModuleData(moduleName: '热门推荐', type: -101, showType: 2),
...game,
];
HomePlateModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
homePage = _modules(json['homePage']);
deepWeb = _modules(json['deepWeb']);
community = _modules(json['community']);
novel = _modules(json['novel']);
pics = _modules(json['pics']);
game = _modules(json['game']);
dramaPage = _modules(json['dramaPage']);
}
//字段缺失或类型不对(后端偶发下发非数组)都退化成空列表,别让首页解析整个抛掉
static List<ModuleData> _modules(dynamic list) =>
(list is List ? list : const [])
.map((e) => ModuleData.fromJson(e))
.toList();
}
@@ -0,0 +1,19 @@
import '../video_model.dart';
///推荐界面信息返回
class RecommendListRes {
List<VideoModel>? vInfo;
bool? hasNext;
/// 推荐队列版本,仅用于日志排查
String? queueVersion;
/// 视频列表字段各接口叫法不一,按 list → videos → vInfos 依次取
RecommendListRes.fromJson(Map<String, dynamic>? json) {
json ??= {};
final list = json['list'] ?? json['videos'] ?? json['vInfos'];
vInfo = (list as List?)?.map((e) => VideoModel.fromJson(e)).toList();
hasNext = json['hasNext'];
queueVersion = json['queueVersion']?.toString();
}
}
@@ -0,0 +1,39 @@
/// 首页内容更新红点标记(GET /content/update-markers
class HomeUpdateMarkersResp {
String? homeLatestAt;
String? todayLatestAt;
List<HomeModuleUpdateMarker>? modules;
HomeUpdateMarkersResp({
this.homeLatestAt,
this.todayLatestAt,
this.modules,
});
factory HomeUpdateMarkersResp.fromJson(Map<String, dynamic>? json) {
json ??= {};
return HomeUpdateMarkersResp(
homeLatestAt: json['homeLatestAt']?.toString(),
todayLatestAt: json['todayLatestAt']?.toString(),
modules: (json['modules'] as List?)
?.whereType<Map>()
.map((e) => HomeModuleUpdateMarker.fromJson(Map<String, dynamic>.from(e)))
.toList(),
);
}
}
class HomeModuleUpdateMarker {
String? moduleId;
String? latestAt;
HomeModuleUpdateMarker({this.moduleId, this.latestAt});
factory HomeModuleUpdateMarker.fromJson(Map<String, dynamic>? json) {
json ??= {};
return HomeModuleUpdateMarker(
moduleId: json['moduleId']?.toString(),
latestAt: json['latestAt']?.toString(),
);
}
}
@@ -0,0 +1,90 @@
import '../cartoon_media_info.dart';
import '../video_model.dart';
import 'module_detail_model.dart';
/// 片库筛选面板的全部可选项(分类 / 排序 / 付费 / 标签 / 时间)
class HomeVideoLibrary {
List<TimeType>? canvas;
List<TimeType>? orderBy;
List<TimeType>? paymentType;
List<Tags>? vidTags;
List<Tags>? acgTags;
List<TimeType>? timeType;
HomeVideoLibrary();
HomeVideoLibrary.fromJson(dynamic json) {
canvas = (json['canvas'] as List?)?.map((e) => TimeType.fromJson(e)).toList();
orderBy = (json['orderBy'] as List?)?.map((e) => TimeType.fromJson(e)).toList();
paymentType = (json['paymentType'] as List?)?.map((e) => TimeType.fromJson(e)).toList();
vidTags = (json['vidTags'] as List?)?.map((e) => Tags.fromJson(e)).toList();
acgTags = (json['acgTags'] as List?)?.map((e) => Tags.fromJson(e)).toList();
timeType = (json['timeType'] as List?)?.map((e) => TimeType.fromJson(e)).toList();
}
//一项都没有就别渲染筛选面板了;timeType 不算,它单独有默认值
bool get isNotEmpty =>
canvas?.isNotEmpty == true ||
orderBy?.isNotEmpty == true ||
paymentType?.isNotEmpty == true ||
acgTags?.isNotEmpty == true ||
vidTags?.isNotEmpty == true;
}
/// 筛选面板里的一个选项,key 传给接口、name 给用户看
class TimeType {
String? key;
String? name;
TimeType.fromJson(dynamic json) {
key = json['key'];
name = json['name'];
}
bool get isACG => name?.contains("动漫") == true || name?.contains("漫画") == true;
Map<String, dynamic> toJson() => {'key': key, 'name': name};
}
/// 用户当前选中的筛选组合,整个塞进请求参数
class Keyword {
TimeType? canvas;
TimeType? orderBy;
Tags? tags;
TimeType? paymentType;
TimeType? timeType;
Keyword();
Keyword.fromJson(dynamic json) {
canvas = json['canvas'] != null ? TimeType.fromJson(json['canvas']) : null;
orderBy = json['orderBy'] != null ? TimeType.fromJson(json['orderBy']) : null;
tags = json['tags'] != null ? Tags.fromJson(json['tags']) : null;
paymentType = json['paymentType'] != null ? TimeType.fromJson(json['paymentType']) : null;
timeType = json['timeType'] != null ? TimeType.fromJson(json['timeType']) : null;
}
//拼给调用方比对「筛选条件有没有变」,不作他用
String get modelKey => "${canvas?.name}${orderBy?.name}${tags?.name}${paymentType?.name}${timeType?.name}";
Map<String, dynamic> toJson() => {
'canvas': canvas?.toJson(),
'orderBy': orderBy?.toJson(),
'tags': tags?.toJson(),
'paymentType': paymentType?.toJson(),
'timeType': timeType?.toJson(),
}..removeWhere((key, value) => value == null);
}
/// 片库筛选结果:影视走 listACG 走 allMediaList
class HomeVideoLibraryResult {
bool? hasNext;
List<VideoModel>? list;
List<CartoonMediaInfo>? allMediaList;
HomeVideoLibraryResult.fromJson(dynamic json) {
hasNext = json['hasNext'];
list = (json['list'] as List?)?.map((e) => VideoModel.fromJson(e)).toList();
allMediaList = (json['allMediaList'] as List?)?.map((e) => CartoonMediaInfo.fromJson(e)).toList();
}
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:hgdj/hj_model/home/module_detail_model.dart';
import 'package:hgdj/hj_model/video_model.dart';
//注意⚠️ VideoListResp 只用于兼容不同业务服务端返回的视频列表 key 不一致的情况
//注意⚠️ 请勿添加其他类型的数据list,如需添加请单独另外创建新的类
class VideoListResp {
List<VideoModel>? videos;
bool? hasNext;
/// 搜索返回的标签视频
List<VideoModel>? tagVidList;
String? tagID;
VideoListResp.fromJson(Map<String, dynamic>? json) {
json ??= {};
hasNext = json['hasNext'];
//各业务的视频列表 key 不一样,逐个匹配(后命中的覆盖先命中的)
for (final key in [
'videos',
'vInfos',
'searchVideo',
'list',
'data',
'searchTag'
]) {
if (json[key] is List) {
videos =
(json[key] as List).map((e) => VideoModel.fromJson(e)).toList();
}
}
tagID = json['tagID'] ?? json['libraryTag']?['id'];
if (json['tagVidList'] is List) {
tagVidList = (json['tagVidList'] as List)
.map((e) => VideoModel.fromJson(e))
.toList();
}
}
}
//注意⚠️ 别并进 ListBaseModel:调用方(SectionAllLogic)靠 hasNext 的 null 区分「后端没下发」和「明确没有更多」,
//而 ListBaseModel 会把 null 压成 false,专题全部页会误判成没有下一页、停在第一页
class SectionListResp {
List<AllSection>? list;
bool? hasNext;
SectionListResp.fromJson(Map<String, dynamic>? json) {
json ??= {};
hasNext = json['hasNext'];
if (json['list'] is List) {
list = (json['list'] as List).map((e) => AllSection.fromJson(e)).toList();
}
}
}