初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
import 'package:hgdj/hj_model/actress_info.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/text_util.dart';
import '../cartoon_media_info.dart';
class AllMediaInfo {
List<Content>? contents;
int? countBrowse;
int? countCollect;
int? countComment;
int? countDisLike;
int? countLike;
int? countPurchases;
int? countView;
String? createdAt;
int? direction;
int? freeTime;
bool? hasFollow;
String? horizontalCover;
List<ActressInfo>? allActress;
String? get coverH {
return (horizontalCover?.isNotEmpty == true)
? horizontalCover
: verticalCover;
}
String? get coverV {
return (verticalCover?.isNotEmpty == true)
? verticalCover
: horizontalCover;
}
String? id;
int? kind;
String? mId;
VidStatus? mediaStatus;
String? mediaType;
String? moduleName;
int? number;
int? permission;
bool? permissionIconHide;
int? price;
String? sId;
String? sectionName;
int? sectionSort;
int? sellType;
int? sortCode;
int? status;
String? summary;
List<TagsBean>? tagDetails;
List<String>? tags;
String? title;
int? totalEpisode; //总集数
int? currentEpisode; //当前集数
int? updateStatus; //0 默认 1、更新中 2、已完结
String? updateTime;
String? verticalCover;
AllMediaInfo({
this.contents,
this.countBrowse,
this.countCollect,
this.countComment,
this.countDisLike,
this.countLike,
this.countPurchases,
this.countView,
this.allActress,
this.createdAt,
this.direction,
this.freeTime,
this.hasFollow,
this.horizontalCover,
this.id,
this.kind,
this.mId,
this.mediaStatus,
this.mediaType,
this.moduleName,
this.number,
this.permission,
this.permissionIconHide,
this.price,
this.sId,
this.sectionName,
this.sectionSort,
this.sellType,
this.sortCode,
this.status,
this.summary,
this.tagDetails,
this.tags,
this.title,
this.totalEpisode,
this.currentEpisode,
this.updateStatus,
this.updateTime,
this.verticalCover,
});
//集数状态描述
String get episodeNumberStatus {
if (updateStatus == 2) {
return '$totalEpisode话';
} else {
return '更新${currentEpisode != 0 ? currentEpisode : totalEpisode}';
}
}
//更新状态
String get updateDesc {
if (updateStatus == 2) {
return '已完结';
} else {
return '连载中';
}
}
AllMediaInfo.fromJson(Map<String, dynamic> json) {
contents = json["contents"] == null
? []
: List<Content>.from(json["contents"]!.map((x) => Content.fromJson(x)));
countBrowse = json["countBrowse"];
countCollect = json["countCollect"];
countComment = json["countComment"];
countDisLike = json["countDisLike"];
countLike = json["countLike"];
countPurchases = json["countPurchases"];
countView = json["countView"];
createdAt = json["createdAt"];
direction = json["direction"];
freeTime = json["freeTime"];
hasFollow = json["hasFollow"];
horizontalCover = json["horizontalCover"];
allActress = json["actresses"] == null
? []
: List<ActressInfo>.from(
json["actresses"]!.map((x) => ActressInfo.fromJson(x)));
id = json["id"];
kind = json["kind"];
mId = json["mId"];
mediaStatus = json["mediaStatus"] == null
? null
: VidStatus.fromMap(json["mediaStatus"]);
mediaType = json["mediaType"];
moduleName = json["moduleName"];
number = json["number"];
permission = json["permission"];
permissionIconHide = json["permissionIconHide"];
price = json["price"];
sId = json["sId"];
sectionName = json["sectionName"];
sectionSort = json["sectionSort"];
sellType = json["sellType"];
sortCode = json["sortCode"];
status = json["status"];
summary = json["summary"];
tagDetails = json["tagDetails"] == null
? []
: List<TagsBean>.from(
json["tagDetails"]!.map((x) => TagsBean.fromMap(x)));
tags = parseStringList(json["tags"]);
title = json["title"];
totalEpisode = json["totalEpisode"];
currentEpisode = json['currentEpisode'];
updateStatus = json["updateStatus"];
updateTime = json["updateTime"];
verticalCover = json["verticalCover"];
}
}
class Content {
String? audioUrl;
int? countBrowse;
int? countCollect;
int? countComment;
int? countDisLike;
int? countLike;
int? countPurchases;
String? createdAt;
int? episodeNumber;
int? height;
String? id;
int? listenPermission;
String? md5;
String? mediaId;
int? mediaSize;
VidStatus? mediaStatus;
String? name;
int? playTime;
int? price;
double? ratio;
String? text;
String? updateTime;
List<String>? urlSet;
String? videoUrl;
int? weight;
Content({
this.audioUrl,
this.countBrowse,
this.countCollect,
this.countComment,
this.countDisLike,
this.countLike,
this.countPurchases,
this.createdAt,
this.episodeNumber,
this.height,
this.id,
this.listenPermission,
this.md5,
this.mediaId,
this.mediaSize,
this.mediaStatus,
this.name,
this.playTime,
this.price,
this.ratio,
this.text,
this.updateTime,
this.urlSet,
this.videoUrl,
this.weight,
});
Content.fromJson(Map<String, dynamic> json) {
audioUrl = json["audioUrl"];
countBrowse = json["countBrowse"];
countCollect = json["countCollect"];
countComment = json["countComment"];
countDisLike = json["countDisLike"];
countLike = json["countLike"];
countPurchases = json["countPurchases"];
createdAt = json["createdAt"];
episodeNumber = json["episodeNumber"];
height = json["height"];
id = json["id"];
listenPermission = json["listenPermission"];
md5 = json["md5"];
mediaId = json["mediaId"];
mediaSize = json["mediaSize"];
mediaStatus = json["mediaStatus"] == null
? null
: VidStatus.fromMap(json["mediaStatus"]);
name = json["name"];
playTime = json["playTime"];
price = json["price"];
ratio = double.tryParse(json['ratio']?.toString() ?? '');
text = json["text"];
updateTime = json["updateTime"];
urlSet = parseStringList(json["urlSet"]);
videoUrl = json["videoUrl"];
weight = json["weight"];
}
Map<String, dynamic> toJson() => {
"audioUrl": audioUrl,
"countBrowse": countBrowse,
"countCollect": countCollect,
"countComment": countComment,
"countDisLike": countDisLike,
"countLike": countLike,
"countPurchases": countPurchases,
"createdAt": createdAt,
"episodeNumber": episodeNumber,
"height": height,
"id": id,
"listenPermission": listenPermission,
"md5": md5,
"mediaId": mediaId,
"mediaSize": mediaSize,
"mediaStatus": mediaStatus?.toJson(),
"name": name,
"playTime": playTime,
"price": price,
"ratio": ratio,
"text": text,
"updateTime": updateTime,
"urlSet":
urlSet == null ? [] : List<dynamic>.from(urlSet!.map((x) => x)),
"videoUrl": videoUrl,
"weight": weight,
};
}
class MediaSearchListModel {
bool? hasNext;
List<CartoonMediaInfo>? list;
List<CartoonMediaInfo>? tagMedia;
String? tagId;
int? total;
MediaSearchListModel({this.hasNext, this.list, this.total, this.tagId});
MediaSearchListModel.fromJson(dynamic json) {
hasNext = json['hasNext'];
list = (json['list'] as List?)
?.map((v) => CartoonMediaInfo.fromJson(v))
.toList();
tagMedia = (json['tagMedia'] as List?)
?.map((v) => CartoonMediaInfo.fromJson(v))
.toList();
tagId = json['tagId'];
total = json['total'];
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['hasNext'] = hasNext;
map['list'] = list?.map((v) => v.toJson()).toList();
map['total'] = total;
return map;
}
}
@@ -0,0 +1,71 @@
//漫画章节
import 'package:hgdj/config/address.dart' as address;
import 'package:path/path.dart' as path;
import '../media_content.dart';
class ComicChapterInfo {
int? episodeNumber;
bool? hasBuy;
String? id;
int? listenPermission; // 0:会员 1:金币购买 2:免费
String? mediaId;
String? name;
int? price;
String? createdAt;
String? cover;
String? audioUrl;
bool inFreeEpisode = false; //是否属于免费集数(前 freeEpisode 集)
MediaContent? mediaContent; //(另外的接口数据返回,外部手动赋值)
// 有声小说
String get getRealAudioUrl {
if (audioUrl == null || audioUrl!.isEmpty) {
return "";
}
if (!audioUrl!.startsWith("http") && !audioUrl!.startsWith("https")) {
return path.join(address.Address.audioCdnAddress ?? '', audioUrl);
}
return audioUrl!;
}
ComicChapterInfo({
this.episodeNumber,
this.hasBuy,
this.id,
this.listenPermission,
this.mediaId,
this.name,
this.price,
this.createdAt,
this.cover,
this.audioUrl,
});
ComicChapterInfo.fromJson(Map<String, dynamic> json) {
episodeNumber = json['episodeNumber'];
hasBuy = json['hasBuy'];
id = json['id'];
listenPermission = json['listenPermission'];
mediaId = json['mediaId'];
name = json['name'];
price = json['price'];
createdAt = json['createdAt'];
cover = json['cover'];
audioUrl = json['audioUrl'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['episodeNumber'] = episodeNumber;
data['hasBuy'] = hasBuy;
data['id'] = id;
data['listenPermission'] = listenPermission;
data['mediaId'] = mediaId;
data['name'] = name;
data['price'] = price;
return data;
}
}
+130
View File
@@ -0,0 +1,130 @@
import 'package:hgdj/hj_utils/text_util.dart';
class ActressInfo {
String? age;
String? alias;
String? birthday;
String? bloodType;
String? chineseName;
int? collectCount;
num? commentCount;
String? cupSize;
String? debutTime;
String? englishName;
num? galleryCount;
String? gender;
num? growCount;
String? height;
String? id;
int? likeCount;
String? name;
String? portrait;
String? region;
String? score;
List<String>? seriesCover;
String? summary;
String? threeDimensions;
int? type; // //1、女优 2、网黄 3、博主 4、声优
int? videoCount;
String? weight;
String? office;
int? picsCount;
int? dislikeCount;
bool? hasLiked;
bool? hasDisliked;
bool? hasCollected;
int? viewCount;
bool? hasSubscribe;
String get heightThreeDimen {
if (threeDimensions?.isNotEmpty == true && height?.isNotEmpty == true) {
return "身高三围: T$height $threeDimensions";
}
if (height?.isNotEmpty == true) {
return "身高: T$height";
}
if (threeDimensions?.isNotEmpty == true) {
return "三围: $threeDimensions";
}
return "";
}
ActressInfo();
int get workCount {
return (videoCount ?? 0) + (picsCount ?? 0);
}
String get actressName {
return name ?? '';
}
ActressInfo.fromJson(dynamic json) {
age = json['age'];
alias = json['alias'];
birthday = json['birthday'];
bloodType = json['bloodType'];
chineseName = json['chineseName'];
collectCount = json['collectCount'];
commentCount = json['commentCount'];
cupSize = json['cupSize'].toString();
debutTime = json['debutTime'];
englishName = json['englishName'];
galleryCount = json['galleryCount'];
gender = json['gender'];
growCount = json['growCount'];
height = json['height'];
id = json['id'];
likeCount = json['likeCount'];
name = json['name'];
portrait = json['portrait'];
region = json['region'];
score = json['score'];
seriesCover = parseStringList(json['seriesCover']);
summary = json['summary'];
threeDimensions = json['threeDimensions'];
type = json['type'];
weight = json['weight'];
videoCount = json['videoCount'];
office = json['office'];
picsCount = json['picsCount'];
dislikeCount = json['dislikeCount'];
hasLiked = json['hasLiked'];
hasDisliked = json['hasDisliked'];
hasCollected = json['hasCollected'];
viewCount = json['viewCount'];
hasSubscribe = json['hasSubscribe'];
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['age'] = age;
map['alias'] = alias;
map['birthday'] = birthday;
map['bloodType'] = bloodType;
map['chineseName'] = chineseName;
map['collectCount'] = collectCount;
map['commentCount'] = commentCount;
map['cupSize'] = cupSize.toString();
map['debutTime'] = debutTime;
map['englishName'] = englishName;
map['galleryCount'] = galleryCount;
map['gender'] = gender;
map['growCount'] = growCount;
map['height'] = height;
map['id'] = id;
map['likeCount'] = likeCount;
map['name'] = name;
map['portrait'] = portrait;
map['region'] = region;
map['score'] = score;
map['seriesCover'] = seriesCover;
map['summary'] = summary;
map['threeDimensions'] = threeDimensions;
map['type'] = type;
map['videoCount'] = videoCount;
map['weight'] = weight;
map['office'] = office;
return map;
}
}
+22
View File
@@ -0,0 +1,22 @@
import 'package:hgdj/hj_model/actress_info.dart';
class ActressModel {
ActressModel({
this.hasFollow,
this.info,
});
ActressModel.fromJson(dynamic json) {
hasFollow = json['hasFollow'];
if (json['info'] != null) {
info = ActressInfo.fromJson(json['info']);
}
if (json['desc'] is List) {
desc = (json['desc'] as List).map((e) => e.toString()).toList();
}
}
bool? hasFollow;
ActressInfo? info;
List<String>? desc;
}
@@ -0,0 +1,38 @@
/// 评论区置顶 BannerGET /banner/list?scene=COMMENT_TOP
class CommentTopBannerModel {
String? id;
String? imageUrl;
String? mediaType; // IMAGE | GIF
String? linkType; // INTERNAL | EXTERNAL | NONE
String? linkValue;
int? sort;
CommentTopBannerModel({
this.id,
this.imageUrl,
this.mediaType,
this.linkType,
this.linkValue,
this.sort,
});
bool get hasLink {
final t = linkType?.toUpperCase();
return t == 'INTERNAL' || t == 'EXTERNAL';
}
bool get isGif =>
mediaType?.toUpperCase() == 'GIF' || (imageUrl?.toLowerCase().contains('.gif') ?? false);
factory CommentTopBannerModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CommentTopBannerModel(
id: json['id']?.toString(),
imageUrl: json['imageUrl']?.toString(),
mediaType: json['mediaType']?.toString(),
linkType: json['linkType']?.toString(),
linkValue: json['linkValue']?.toString(),
sort: json['sort'] is int ? json['sort'] as int : int.tryParse('${json['sort'] ?? ''}'),
);
}
}
+369
View File
@@ -0,0 +1,369 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_model/splash/ads_model.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'acg/comic_chapters_model.dart';
import 'actress_info.dart';
import 'media_content.dart';
class CartoonMediaInfo {
// —— 基础信息 ——
String? id; //文档id
String? title; //标题
String? author; //作者
String? summary; //简介
String? mediaType; //媒体类型 "video":动漫,"image":漫画 "text":小说
int? mediaSubType; //子类型 ,暂时只有小说使用 0-默认文本小说 1-有声小说
int? kind; //种类(1、哩番,2、3D,3、同人动画,4、单行本,5、连载漫画,6、韩漫 )
int? status; //状态
VidStatus? mediaStatus; //媒体状态
String? lsjId; // 老司机媒体资源ID
// —— 封面 ——
String? horizontalCover; //横版封面
String? verticalCover; //竖版封面
// —— 集数 / 更新 ——
int? totalEpisode; //总集数
int? currentEpisode; //当前集数
int? freeEpisode; //免费集数
bool isCartoonFreeEpisode =
false; //当前播放集是否属于免费集(前 freeEpisode 集):运行时按当前集号计算,命中则不拦播、进度可全程拖
int? updateStatus; //更新状态 0 默认 1、更新中 2、已完结
String? updateTime; //文档更新时间
String? createdAt; //文档创建时间
// —— 价格 / 权限 ——
int? price; //价格(整部购买的类型 小说 cos 写真)
int? discountPrice; // 折扣价(后端字段,App 未使用)
int? contentsPrice; //所有子集的价格
int? permission; // 0:会员 1:金币购买 2:免费
bool? permissionIconHide; //售卖类型标识隐藏
// —— 专题 / 模块 ——
String? mId; //模块ID
String? moduleName; //模块名称
String? sId; //专题id
String? sectionName; //专题名称
int? sectionSort; //专题排序
int? sortCode; //排序
// —— 统计 ——
int? countBrowse; //浏览数
int? countCollect; //收藏数
int? countDisLike; //不喜欢数
int? countComment; //评论数
int? countLike; //喜欢数
int? countPurchases; //购买数
int? countView; //展现数
bool? hasFollow; //是否关注
// —— 标签 / 演员 / 内容 ——
List<TagsBean>? tagDetails; //标签对象
List<ActressInfo>? allActress; // 演员列表
ActressInfo? actress; // 主演员
List<MediaContent>? contents; // 子集内容列表
AdsInfoModel? randomAdsInfo; // 随机广告
// —— 其他后端字段 ——
int? direction; // 方向(横/竖)
int? freeTime; // 免费试看时长
bool? isDelete; // 是否已删除
int? choiceSort; // 后端字段,App 未使用
int? countPurchasesRate; // 后端字段,App 未使用
String? updatedAct; // 后端字段,App 未使用
// —— 本地 / UI 字段(非后端返回) ——
List<ComicChapterInfo>? episodeList; // 通过接口获取,外部赋值(改版了,不是同一个接口返回)
int? episodeCurPage; // 当前页码
int episodePageSize = 40; // 每页集数
int isUpSort = 0; //0-正序 1-倒序
bool haxNextEpisode = true; // 是否还有下一页
int? episodeNumber; //本地添加字段。用于判断,跳转到播放详情的index
bool isSelected = false; // 是否选中
String? heroTag; //本地字段,用于传递hero动画tag 随机数
String? get coverH =>
horizontalCover?.isNotEmpty == true ? horizontalCover : verticalCover;
String? get coverV =>
verticalCover?.isNotEmpty == true ? verticalCover : horizontalCover;
String get unit => mediaType == 'image' ? '' : '';
//更新状态
String get updateDesc => updateStatus == 1 ? '连载中' : '已完结';
String get tagIds => tagDetails?.map((e) => e.id).toList().join(',') ?? '';
String get tagNames =>
tagDetails?.map((e) => e.name).toList().join(',') ?? '';
//更新状态
String get getDetailUpdateString {
if (updateStatus == 2 || updateStatus == 0) {
return '已完结';
} else {
return '连载中';
}
}
Color get getDetailUpdateColor {
if (updateStatus == 2 || updateStatus == 0) {
return Color(0xff989898);
} else {
return AppColors.actionRed;
}
}
//是否是有声小说
bool get isAudiobooks => mediaType == 'text' && mediaSubType == 1;
//集数状态描述
String get episodeNumberStatus {
if (updateStatus == 1) {
if (currentEpisode == 0) {
return '$totalEpisode$unit';
} else {
return '更新$currentEpisode$unit';
}
} else {
return '$totalEpisode$unit';
}
}
//评论类型,传参数类型
String get commentType => 'cartoon';
/// 该集是否落在前 N 集免费区间内。
/// 集号缺失兜底为 1(与项目其他处 `episodeNumber ?? 1` 口径一致);服务端真给 0 或负数时判为非免费
bool isFreeEpisodeNumber(int? episodeNumber) {
final ep = episodeNumber ?? 1;
return ep >= 1 && (freeEpisode ?? 0) >= ep;
}
/// 转成播放器用的 [VideoModel][episode] 为当前集
/// 顺带把 [isCartoonFreeEpisode] 按当前集号刷新一次(有副作用,别当纯转换用)
VideoModel toVideoModel(ComicChapterInfo? episode) {
final model = VideoModel()
..videoType = 1
..id = id
..totalEpisode = totalEpisode
..updateStatus = updateStatus
..mediaType = mediaType
..mediaInfo = this
..title = title
..tags = tagDetails
..cover = coverH
..likeCount = countCollect // 视频likecount是收藏数
..freeTime = freeTime ?? 0
..collectCount = countCollect
..commentCount = countComment
..playCount = countBrowse
..freeArea = permission == 2
..coins = price
..originCoins = price
..vidStatus = (VidStatus()
..hasPaid = mediaStatus?.hasPaid == true
..hasCollected = mediaStatus?.hasCollected
..hasLiked = mediaStatus?.hasLiked
..hasDisliked = mediaStatus?.hasDisliked);
// 动漫免费集:当前集号在前 freeEpisode 集内 → 播放不拦、进度可全程拖(与 video_logic 免费章节判定同一口径)
isCartoonFreeEpisode = isFreeEpisodeNumber(episode?.episodeNumber);
final list = episodeList ?? [];
if (list.isNotEmpty) {
//在列表里找不到当前集就退回第一集,别让播放信息整块空掉
final index = list.indexWhere((e) => e.id == episode?.id);
final content = list[index < 0 ? 0 : index].mediaContent;
model
..subid = content?.id
..name = content?.name
..sourceURL = content?.videoUrl
..playTime = content?.playTime;
}
return model;
}
//acg子集角标显示逻辑,0:会员 1:金币购买 2:免费。为 false(无任何权限)时才展示角标
bool get hasPermission {
return (mediaStatus?.hasPaid == true && permission == 1) ||
permission == 2 ||
(permission == 0 && globalStore.isVIP) ||
globalStore.isSuperUp;
}
CartoonMediaInfo(
{this.contents,
this.choiceSort,
this.countBrowse,
this.countCollect,
this.countComment,
this.countLike,
this.countPurchases,
this.countPurchasesRate,
this.countView,
this.createdAt,
this.direction,
this.freeTime,
this.horizontalCover,
this.id,
this.isDelete,
this.kind,
this.mId,
this.mediaType,
this.moduleName,
this.permission,
this.permissionIconHide,
this.price,
this.sId,
this.discountPrice,
this.sectionName,
this.sectionSort,
this.sortCode,
this.status,
this.summary,
this.tagDetails,
this.title,
this.totalEpisode,
this.currentEpisode,
this.updateStatus,
this.updateTime,
this.updatedAct,
this.verticalCover,
this.author,
this.freeEpisode,
this.contentsPrice,
this.mediaSubType,
this.actress});
CartoonMediaInfo.fromJson(dynamic json) {
freeEpisode = json['freeEpisode'];
mediaSubType = json['mediaSubType'];
author = json['author'];
choiceSort = json['choiceSort'];
totalEpisode = json['totalEpisode'];
currentEpisode = json['currentEpisode'];
countBrowse = json['countBrowse'];
countCollect = json['countCollect'];
countComment = json['countComment'];
countLike = json['countLike'];
countDisLike = json['countDisLike'];
countPurchases = json['countPurchases'];
countPurchasesRate = json['countPurchasesRate'];
discountPrice = json['discountPrice'];
countView = json['countView'];
createdAt = json['createdAt'];
direction = json['direction'];
freeTime = json['freeTime'];
horizontalCover = json['horizontalCover'];
id = json['id'];
isDelete = json['isDelete'];
kind = json['kind'];
mId = json['mId'];
mediaType = json['mediaType'];
moduleName = json['moduleName'];
permission = json['permission'];
permissionIconHide = json['permissionIconHide'];
price = json['price'];
sId = json['sId'];
sectionName = json['sectionName'];
sectionSort = json['sectionSort'];
sortCode = json['sortCode'];
status = json['status'];
summary = json['summary'];
lsjId = json['lsjId'];
actress =
json["actress"] == null ? null : ActressInfo.fromJson(json["actress"]);
allActress = json["actresses"] == null
? []
: List<ActressInfo>.from(
json["actresses"]!.map((x) => ActressInfo.fromJson(x)));
hasFollow = json['hasFollow'];
if (json['tagDetails'] != null) {
tagDetails = [];
json['tagDetails'].forEach((v) {
tagDetails?.add(TagsBean.fromMap(v));
});
}
if (json['contents'] != null) {
contents = [];
json['contents'].forEach((v) {
contents?.add(MediaContent.fromJson(v));
});
}
if (json['title'] == null) {
title = json['name'];
} else {
title = json['title'];
}
updateStatus = json['updateStatus'];
updateTime = json['updateTime'];
updatedAct = json['updatedAct'];
verticalCover = json['verticalCover'];
mediaStatus = VidStatus.fromMap(json['mediaStatus']);
contentsPrice = json['contentsPrice'];
}
///videoModel是否随机广告
bool isRandomAd() {
return randomAdsInfo != null;
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['mediaSubType'] = mediaSubType;
map['choiceSort'] = choiceSort;
map['countBrowse'] = countBrowse;
map['countCollect'] = countCollect;
map['countComment'] = countComment;
map['countLike'] = countLike;
map['countDisLike'] = countDisLike;
map['countPurchases'] = countPurchases;
map['countPurchasesRate'] = countPurchasesRate;
map['countView'] = countView;
map['createdAt'] = createdAt;
map['direction'] = direction;
map['freeTime'] = freeTime;
map['horizontalCover'] = horizontalCover;
map['id'] = id;
map['isDelete'] = isDelete;
map['kind'] = kind;
map['mId'] = mId;
map['mediaType'] = mediaType;
map['moduleName'] = moduleName;
map['permission'] = permission;
map['permissionIconHide'] = permissionIconHide;
map['price'] = price;
map['sId'] = sId;
map['sectionName'] = sectionName;
map['sectionSort'] = sectionSort;
map['sortCode'] = sortCode;
map['status'] = status;
map['summary'] = summary;
map['lsjId'] = lsjId;
if (tagDetails != null) {
map['tagDetails'] = tagDetails?.map((v) => v.toJson()).toList();
}
if (contents != null) {
map['contents'] = contents?.map((v) => v.toJson()).toList();
}
map['hasFollow'] = hasFollow;
map['title'] = title;
map['totalEpisode'] = totalEpisode;
map['currentEpisode'] = currentEpisode;
map['updateTime'] = updateTime;
map['updateStatus'] = updateStatus;
map['updatedAct'] = updatedAct;
map['verticalCover'] = verticalCover;
map['mediaStatus'] = mediaStatus;
return map;
}
}
@@ -0,0 +1,38 @@
import 'package:hgdj/hj_model/comment/comment_model.dart';
///评论列表返回信息
class CommentListRes {
List<CommentModel>? list;
int? total;
bool? hasNext = false;
List<CommentLink>? quickSearchList;
CommentListRes.fromJson(Map<String, dynamic>? json) {
json ??= {};
list =
(json['list'] as List?)?.map((e) => CommentModel.fromJson(e)).toList();
total = json['total'];
hasNext = json['hasNext'];
quickSearchList = (json['quickSearchList'] as List?)
?.map((e) => CommentLink.fromJson(e))
.toList();
}
}
///评论区快捷入口
class CommentLink {
String? id;
String? title;
String? searchKeyword;
String? link;
int? type; //1:评论置顶 2:评论大家都在搜
CommentLink.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
title = json['title'];
searchKeyword = json['searchKeyword'];
link = json['link'];
type = json['type'];
}
}
+65
View File
@@ -0,0 +1,65 @@
import 'package:hgdj/hj_model/comment/reply_model.dart';
class CommentModel {
String? id;
//用户信息
int? userID;
String? userName;
String? userPortrait;
bool? superUser;
int? vipLevel;
//评论内容
String? content;
String? image;
String? createdAt;
bool? isGodComment; //神评论
bool? official; //官方评论,读取用 isOfficial
//互动数据
int? likeCount;
int? commCount;
bool? isLike;
//超链接评论(点评论跳搜索/外链)
String? searchKeyword;
int? linkType; //1:内链跳转 2:外链跳转
String? linkStr;
//子回复,分页在本地维护
List<ReplyModel>? replies;
bool? hasMoreReply = false; //还有更多子回复
int? replyPage = 1;
bool get isOfficial => official == true;
//超链接评论隐藏点赞/回复入口 true 隐藏 false 显示
bool get isHideLikeOrComment =>
(linkType == 1 || linkType == 2) &&
linkStr?.isNotEmpty == true &&
searchKeyword?.isNotEmpty == true;
CommentModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
userID = json['userID'];
userName = json['userName'];
userPortrait = json['userPortrait'];
superUser = json['superUser'];
vipLevel = json['vipLevel'];
content = json['content'];
image = json['image'];
createdAt = json['createdAt'];
isGodComment = json['isGodComment'];
official = json['isOfficial'];
likeCount = json['likeCount'];
commCount = json['commCount'];
isLike = json['isLike'];
searchKeyword = json['searchKeyword'];
linkType = json['linkType'];
linkStr = json['linkStr'];
replies =
(json['Info'] as List?)?.map((e) => ReplyModel.fromJson(e)).toList();
}
}
+36
View File
@@ -0,0 +1,36 @@
class ReplyModel {
String? id;
//用户信息
int? userID;
String? userName;
String? userPortrait;
//被回复人
int? toUserID;
String? toUserName;
//回复内容
String? content;
String? image;
String? createdAt;
//互动数据
int? likeCount;
bool? isLike;
ReplyModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
userID = json['userID'];
userName = json['userName'];
userPortrait = json['userPortrait'];
toUserID = json['toUserID'];
toUserName = json['toUserName'];
content = json['content'];
image = json['image'];
createdAt = json['createdAt'];
likeCount = json['likeCount'];
isLike = json['isLike'];
}
}
@@ -0,0 +1,23 @@
///我的评论列表项
class UserCommentItem {
String? id;
String? objID;
String? content;
int? likeCount;
bool? isGodComment;
String? vidTitle;
String? vidCover;
String? createdAt;
UserCommentItem.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
objID = json['objID'];
content = json['content'];
likeCount = json['likeCount'];
isGodComment = json['isGodComment'];
vidTitle = json['vidTitle'];
vidCover = json['vidCover'];
createdAt = json['createdAt'];
}
}
+191
View File
@@ -0,0 +1,191 @@
import 'package:hgdj/hj_model/drama_media_info.dart';
import 'package:hgdj/hj_model/media_content.dart';
/// 一条短剧观看记录:续播位置 + 列表展示用的剧信息快照。本地记录,不走后端,见 [DramaResumeStore]。
/// 落库时整条存成 model_data,所以 [mediaId] 必须写进 json——
/// 基类只回 model_data 一列,读盘时全靠它认领是哪部剧
class DramaResume {
final String mediaId; //哪部剧,同时是行主键
final String contentId; //上次看的分集。定位只认它,别再存「第几集」——分页深度靠边翻边找就够了
final int progressSeconds; //看到第几秒
/// 剧信息快照(封面/剧名/集数)。续播定位用不到,是历史记录页拿它画卡片——
/// 只存 id 的话列表除了 id 什么都渲染不出来
final DramaMediaInfo? drama;
/// 本条写入时刻(毫秒)。历史记录页按它倒序,不看 db 的 create_time——
/// 老数据没这个字段,读出来是 0,排在最后
final int watchedAt;
const DramaResume({
required this.mediaId,
required this.contentId,
required this.progressSeconds,
this.drama,
this.watchedAt = 0,
});
DramaResume.fromJson(dynamic json)
: mediaId = json?['mediaId'] ?? '',
contentId = json?['contentId'] ?? '',
progressSeconds = json?['progressSeconds'] ?? 0,
drama = json?['drama'] == null
? null
: DramaMediaInfo.fromJson(json['drama']),
watchedAt = json?['watchedAt'] ?? 0;
Map<String, dynamic> toJson() => {
'mediaId': mediaId,
'contentId': contentId,
'progressSeconds': progressSeconds,
'drama': drama?.toJson(),
'watchedAt': watchedAt,
};
}
/// 付费墙:文案/价格/余额全部服务端下发,前端不自己拼
class DramaPaywall {
String? checkoutContextId; //本次付费墙上下文,充值/开卡归因和下单都要带
String? title;
int? coinBalance;
int? unlockCoin;
String? coinButtonText;
String? cardButtonText;
DramaPaywall.fromJson(dynamic json) {
json ??= {};
checkoutContextId = json['checkoutContextId'];
title = json['title'];
coinBalance = json['coinBalance'];
unlockCoin = json['unlockCoin'];
coinButtonText = json['coinButtonText'];
cardButtonText = json['cardButtonText'];
}
String get titleUI => title?.isNotEmpty == true ? title! : '更多精彩解锁即享';
String get coinButtonUI => coinButtonText?.isNotEmpty == true
? coinButtonText!
: '${unlockCoin ?? 0}金币解锁';
String get cardButtonUI =>
cardButtonText?.isNotEmpty == true ? cardButtonText! : '开通短剧卡免费看';
}
/// AI短剧 Feed 的一条:某部剧 + 该剧第一集。整页走 ListBaseModel<DramaFeedItem>
class DramaFeedItem {
DramaMediaInfo? media;
MediaContent? content;
DramaFeedItem.fromJson(dynamic json) {
json ??= {};
//缺 media 就不造空模型,上层按 null 过滤掉这条,别让信息流多出一个没剧名没地址的空位
if (json['media'] != null) media = DramaMediaInfo.fromJson(json['media']);
if (json['content'] != null)
content = MediaContent.fromJson(json['content']);
}
}
/// 短剧专题(热门短剧 Tab
class DramaTopic {
String? topicId;
String? name;
String? topicType;
String? systemKey;
int? sort;
int? workCount;
DramaTopic({
this.topicId,
this.name,
this.topicType,
this.systemKey,
this.sort,
this.workCount,
});
DramaTopic.fromJson(dynamic json) {
json ??= {};
topicId = json['topicId']?.toString();
name = json['name']?.toString();
topicType = json['topicType']?.toString();
systemKey = json['systemKey']?.toString();
sort = json['sort'] is int
? json['sort'] as int
: int.tryParse('${json['sort'] ?? ''}');
workCount = json['workCount'] is int
? json['workCount'] as int
: int.tryParse('${json['workCount'] ?? ''}');
}
}
/// 短剧搜索结果(`/media/search` kind=4
class DramaSearchResult {
List<DramaMediaInfo> list = [];
List<DramaMediaInfo> tagMediaList = [];
String? tagID;
bool hasNext = false;
DramaSearchResult.fromJson(dynamic json) {
json ??= {};
hasNext = json['hasNext'] == true;
tagID = json['tagID']?.toString();
list = (json['list'] as List?)
?.map((e) => DramaMediaInfo.fromJson(e))
.toList() ??
[];
tagMediaList = (json['tagMediaList'] as List?)
?.map((e) => DramaMediaInfo.fromJson(e))
.toList() ??
[];
}
}
/// 单集金币解锁的下单结果
class DramaUnlockResult {
String? orderId;
String? mediaId;
String? contentId;
int? paidCoin;
int? coinBalance;
DramaUnlockResult.fromJson(Map<String, dynamic>? json) {
json ??= {};
orderId = json['orderId'];
mediaId = json['mediaId'];
contentId = json['contentId'];
paidCoin = json['paidCoin'];
coinBalance = json['coinBalance'];
}
}
/// 短剧单集下载授权结果。权益/上下架/次数校验和扣次都在服务端一次做完,
/// 客户端只负责拿地址开下载,不再自己算次数
class DramaDownloadAuth {
String? mediaId;
String? contentId;
int? episodeNumber;
String? name;
String? cover;
String? downloadUrl; // H.264 m3u8,可能是相对地址
String? h265DownloadUrl; // H.265 m3u8,没有时为空串
int? mediaSize; // 字节
String? expiresAt; // 本次地址失效时间(签发后 6 小时)
int? remainingDownloadCount; // 扣完之后钱包剩余下载次数,直接用它顶掉本地值
bool? charged; // false = 幂等重试命中,没有重复扣次
DramaDownloadAuth.fromJson(Map<String, dynamic>? json) {
json ??= {};
mediaId = json['mediaId'];
contentId = json['contentId'];
episodeNumber = json['episodeNumber'];
name = json['name'];
cover = json['cover'];
downloadUrl = json['downloadUrl'];
h265DownloadUrl = json['h265DownloadUrl'];
mediaSize = json['mediaSize'];
expiresAt = json['expiresAt'];
remainingDownloadCount = json['remainingDownloadCount'];
charged = json['charged'] == true;
}
}
+226
View File
@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_model/media_content.dart';
import 'package:hgdj/hj_model/video_model.dart';
/// 短剧作品信息(接口 DramaInfo:一部剧 + 一堆分集)
/// 单集播放数据用 [toVideoModel] 转成 VideoModel 交给短视频播放器
class DramaMediaInfo {
// —— 基础信息 ——
String? id; //短剧id
String? title; //剧名
String? author; //作者/出品方
String? summary; //简介
int? kind; //种类
int? style; //样式
int? status; //0 下架 1 上架
int? number; //序号
VidStatus? mediaStatus; //媒体状态(已购/已收藏/已点赞)
String? lsjId; // 老司机媒体资源ID
// —— 封面 ——
String? horizontalCover; //横版封面
String? verticalCover; //竖版封面
// —— 集数 / 更新 ——
int? totalEpisode; //总集数
int? currentEpisode; //已更新到第几集
int? freeEpisode; //默认免费集数,仅后台初始化用;能不能播一律看分集的 canPlay
int? updateStatus; //1 连载中 2 已完结
String? updateTime; //更新时间
String? createdAt; //创建时间
// —— 价格 / 权限 ——
int? price; //整部价格
int? contentsPrice; //所有分集的价格
int? permission; // 0:会员/短剧卡 1:金币购买 2:免费
bool? permissionIconHide; //售卖类型标识隐藏
// —— 专题 / 模块 ——
String? mId; //模块ID
String? moduleName; //模块名称
String? sId; //专题id
String? sectionName; //专题名称
int? sectionSort; //专题排序
int? sortCode; //排序
// —— 统计 ——
int? countBrowse; //浏览数
int? countCollect; //收藏数
int? countDisLike; //不喜欢数
int? countComment; //评论数
int? countLike; //喜欢数
int? countPurchases; //购买数
int? countView; //展现数
bool? hasFollow; //是否关注
// —— 标签 ——
List<TagsBean>? tagDetails; //标签对象
// —— 其他后端字段 ——
int? direction; //方向(横/竖)
int? freeTime; //免费试看时长
// —— 本地 / UI 字段(非后端返回) ——
List<MediaContent>? episodeList; //分集列表,走 /media_content/list 单独拉,外部赋值
int? episodeCurPage; //当前页码
//每页集数:必须与选集面板的分段大小(DramaEpisodeSheet._segmentSize)一致,
//这样第 i 页正好是第 i 段,点哪段补哪页,不会出现「点进去先 10 个再变 30 个」
int episodePageSize = 30;
int isUpSort = 0; //0-正序 1-倒序
bool haxNextEpisode = true; //是否还有下一页
bool isSelected = false; //是否选中
String? get coverH =>
horizontalCover?.isNotEmpty == true ? horizontalCover : verticalCover;
String? get coverV =>
verticalCover?.isNotEmpty == true ? verticalCover : horizontalCover;
String get unit => '';
//更新状态
String get updateDesc => updateStatus == 1 ? '连载中' : '已完结';
String get tagIds => tagDetails?.map((e) => e.id).toList().join(',') ?? '';
String get tagNames =>
tagDetails?.map((e) => e.name).toList().join(',') ?? '';
Color get updateDescColor =>
updateStatus == 1 ? AppColors.actionRed : const Color(0xff989898);
//集数状态描述
String get episodeNumberStatus {
if (updateStatus == 1 && (currentEpisode ?? 0) > 0) {
return '更新$currentEpisode$unit';
}
//没有集数就别出这行——直接插值会渲染成「共null集」
return (totalEpisode ?? 0) > 0 ? '$totalEpisode$unit' : '';
}
//点赞/评论/分享统一传这个类型
String get commentType => 'drama';
/// 转成播放器用的 [VideoModel][episode] 为当前集;列表卡片场景传 null
VideoModel toVideoModel(MediaContent? episode) {
return VideoModel()
..id = id
..dramaInfo = this
..dramaEpisode = episode
..subid = episode?.id
..episodeNo = episode?.episodeNumber
..totalEpisode = totalEpisode
..updateStatus = updateStatus
..title = title
..tags = tagDetails
..name = episode?.name
..cover = episode?.cover?.isNotEmpty == true ? episode?.cover : coverV
..sourceURL = episode?.videoUrl
..h265Url = episode?.h265Url
..playTime = episode?.playTime
..likeCount = countLike // 短剧点赞走 thumbsUp,操作台右侧显示的就是这个数
..freeTime = freeTime ?? 0
..collectCount = countCollect
..commentCount = countComment
..playCount = countBrowse
..freeArea = episode?.isFree == true
..coins = price
..originCoins = price
..vidStatus = (VidStatus()
..hasPaid = mediaStatus?.hasPaid == true
..hasCollected = mediaStatus?.hasCollected
..hasLiked = mediaStatus?.hasLiked
..hasDisliked = mediaStatus?.hasDisliked);
}
DramaMediaInfo();
DramaMediaInfo.fromJson(dynamic json) {
json ??= {};
id = json['id'];
title = json['title'] ?? json['name'];
author = json['author'];
summary = json['summary'];
kind = json['kind'];
style = json['style'];
status = json['status'];
number = json['number'];
lsjId = json['lsjId'];
horizontalCover = json['horizontalCover'];
verticalCover = json['verticalCover'];
totalEpisode = json['totalEpisode'];
currentEpisode = json['currentEpisode'];
freeEpisode = json['freeEpisode'];
updateStatus = json['updateStatus'];
updateTime = json['updateTime'];
createdAt = json['createdAt'];
price = json['price'];
contentsPrice = json['contentsPrice'];
permission = json['permission'];
permissionIconHide = json['permissionIconHide'];
mId = json['mId'];
moduleName = json['moduleName'];
sId = json['sId'];
sectionName = json['sectionName'];
sectionSort = json['sectionSort'];
sortCode = json['sortCode'];
countBrowse = json['countBrowse'];
countCollect = json['countCollect'];
countDisLike = json['countDisLike'];
countComment = json['countComment'];
countLike = json['countLike'];
countPurchases = json['countPurchases'];
countView = json['countView'];
hasFollow = json['hasFollow'];
direction = json['direction'];
freeTime = json['freeTime'];
mediaStatus = VidStatus.fromMap(json['mediaStatus']);
tagDetails =
(json['tagDetails'] as List?)?.map((e) => TagsBean.fromMap(e)).toList();
}
/// 落本地观看记录用(见 [DramaResume.drama])。只序列化后端字段,
/// episodeList / 分页游标那些是运行期状态,进二级页会重新拉,存了也没意义
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'author': author,
'summary': summary,
'kind': kind,
'style': style,
'status': status,
'number': number,
'lsjId': lsjId,
'horizontalCover': horizontalCover,
'verticalCover': verticalCover,
'totalEpisode': totalEpisode,
'currentEpisode': currentEpisode,
'freeEpisode': freeEpisode,
'updateStatus': updateStatus,
'updateTime': updateTime,
'createdAt': createdAt,
'price': price,
'contentsPrice': contentsPrice,
'permission': permission,
'permissionIconHide': permissionIconHide,
'mId': mId,
'moduleName': moduleName,
'sId': sId,
'sectionName': sectionName,
'sectionSort': sectionSort,
'sortCode': sortCode,
'countBrowse': countBrowse,
'countCollect': countCollect,
'countDisLike': countDisLike,
'countComment': countComment,
'countLike': countLike,
'countPurchases': countPurchases,
'countView': countView,
'hasFollow': hasFollow,
'direction': direction,
'freeTime': freeTime,
'mediaStatus': mediaStatus?.toJson(),
'tagDetails': tagDetails?.map((e) => e.toJson()).toList(),
};
}
+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();
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import 'package:hgdj/hj_model/acg/comic_chapters_model.dart';
import 'package:hgdj/hj_model/actress_model.dart';
import 'package:hgdj/hj_model/cartoon_media_info.dart';
import 'package:hgdj/hj_model/comment/reply_model.dart';
import 'package:hgdj/hj_model/comment/user_comment_res.dart';
import 'package:hgdj/hj_model/drama/drama_models.dart';
import 'package:hgdj/hj_model/drama_media_info.dart';
import 'package:hgdj/hj_model/home/collection_model.dart';
import 'package:hgdj/hj_model/media_content.dart';
import 'package:hgdj/hj_model/message/message_dynamic_list.dart';
import 'package:hgdj/hj_model/mine/exchange/bill_item_model.dart';
import 'package:hgdj/hj_model/mine/follow_user_list_model.dart';
import 'package:hgdj/hj_model/mine/promotion_record.dart';
import 'package:hgdj/hj_model/video_model.dart';
import '../hj_page/ai/models/ai_change_face_video_model.dart';
import '../hj_page/ai/models/ai_record_model.dart';
import '../hj_page/ai/models/ai_square_model.dart';
import '../hj_page/live/live_model.dart';
import '../hj_page/mine/make_money/withdraw_details_model.dart';
/// 分页列表通用返回:{ hasNext, total, list }
/// 元素类型由 [T] 决定,解析器在 [_parsers] 里按 Type 注册——**新增 T 记得补一行,漏了 list 就是空的**
class ListBaseModel<T> {
bool? hasNext;
List<T>? list;
int? total;
// 用 Type 对象做 key 而非 T.toString()release 混淆下类名字符串会被改写,
// 字符串匹配失败会导致 list 不被赋值 → 静默“没数据”
static final Map<Type, dynamic Function(dynamic)> _parsers = {
VideoModel: (e) => VideoModel.fromJson(e),
CartoonMediaInfo: (e) => CartoonMediaInfo.fromJson(e),
DramaMediaInfo: (e) => DramaMediaInfo.fromJson(e),
DramaFeedItem: (e) => DramaFeedItem.fromJson(e),
CollectionModel: (e) => CollectionModel.fromJson(e),
ComicChapterInfo: (e) => ComicChapterInfo.fromJson(e),
MediaContent: (e) => MediaContent.fromJson(e),
UserCommentItem: (e) => UserCommentItem.fromJson(e),
Promotion: (e) => Promotion.fromJson(e),
BillItemModel: (e) => BillItemModel.fromJson(e),
AiRecordModel: (e) => AiRecordModel.fromJson(e),
AiChangeFaceVideoMod: (e) => AiChangeFaceVideoMod.fromJson(e),
TagsBean: (e) => TagsBean.fromMap(e),
FollowUserModel: (e) => FollowUserModel.fromJson(e),
ActressModel: (e) => ActressModel.fromJson(e),
MessageDynamicList: (e) => MessageDynamicList.fromJson(e),
ReplyModel: (e) => ReplyModel.fromJson(e),
AISquareItemModel: (e) => AISquareItemModel.fromJson(e),
LiveAnchor: (e) => LiveAnchor.fromJson(e),
IncomeModel: (e) => IncomeModel.fromJson(e),
};
ListBaseModel.fromJson(Map<String, dynamic>? map) {
map ??= {};
total = map['total'];
hasNext = map['hasNext'] ?? false;
final parse = _parsers[T];
if (parse == null) return;
final raw = map['list'];
list = raw is List ? raw.map(parse).cast<T>().toList() : <T>[];
}
}
+167
View File
@@ -0,0 +1,167 @@
import 'package:hgdj/config/address.dart' as address;
import 'package:hgdj/hj_model/drama/drama_models.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:path/path.dart' as path;
/// 作品的「一集」:漫画一话 / 小说一章 / 有声一集 / 短剧一集 共用这一个模型,
/// 所以字段是几种业务的并集,取值时按自己的业务挑(如短剧只关心视频与解锁那几个)
class MediaContent {
// ===== 标识 =====
String? id; //本集 id
String? mediaId; //所属作品(剧/漫画/小说)的 id
int? episodeNumber; //第几集/第几话,从 1 开始
String? name; //集名,如「第1集」
String? cover; //本集封面,短剧多为空、回退用剧封面
// ===== 资源 =====
String? videoUrl; //视频相对路径(m3u8),要拼 baseApiPath 才能播
String? h265Url; //H.265 源,为空表示这一集没有 265
String? audioUrl; //有声书音频,见 realAudioUrl
List<String>? urlSet; //多清晰度地址集合,短剧下发空数组
String? text; //小说正文
String? md5;
int? playTime; //时长(秒)
int? mediaSize; //文件字节数
int? height; //视频高
int? weight; //视频宽(后端字段就叫 weight,不是重量)
double? ratio; //宽高比,0 表示后端没算
// ===== 权限 / 付费 =====
//0会员 1金币购买 2免费。**只用于展示**,能不能播看 canPlay
int? listenPermission;
int? price; //单集金币价,0 为免费
bool? hasBuy; //本集是否已单独购买过
bool? isFree; //本集是否免费,别再用整部的 freeEpisode 自己算
bool? canPlay; //能不能播只认这个字段
String? accessType; // free 免费 / bought 已购 / card 短剧卡 / coin 未解锁
DramaPaywall? paywall; //付费墙文案与价格,canPlay=false 时必有,能播时为 null
bool? isActive; //是否上架,下架的集不该出现在选集面板
// ===== 试看(feed / list / info 三个接口都下发)=====
bool? previewEnabled; //这一集配没配试看
int? previewStart; //试看起播秒数,0 = 从头
int? previewSeconds; //试看时长(秒),从 previewStart 起算,到点停下挂付费墙
String? previewVideoUrl; //试看片地址,与 videoUrl 同格式
String? previewH265Url; //试看片的 265 源,为空表示没有
// ===== 统计 =====
int? countBrowse;
int? countCollect;
int? countComment;
int? countDisLike;
int? countLike;
int? countPurchases;
VidStatus? mediaStatus; //本集的已购/已收藏/已点赞状态
// ===== 时间 =====
String? createdAt;
String? updateTime;
//只用来造空 model 占位(拉详情失败时页面据此走错误态),字段一律走 fromJson 赋值
MediaContent();
MediaContent.fromJson(dynamic json) {
json ??= {};
id = json['id'];
mediaId = json['mediaId'];
episodeNumber = json['episodeNumber'];
name = json['name'];
cover = json['cover'];
videoUrl = json['videoUrl'];
h265Url = json['h265Url'];
audioUrl = json['audioUrl'];
//用 is List 判而不是 as List:下发 null 或非数组时硬 cast 会抛,整条分集就解析不出来
final rawUrlSet = json['urlSet'];
urlSet =
rawUrlSet is List ? rawUrlSet.map((e) => e.toString()).toList() : [];
text = json['text'];
md5 = json['md5'];
playTime = json['playTime'];
mediaSize = json['mediaSize'];
height = json['height'];
weight = json['weight'];
//数字直接转;后端也可能下发字符串("1.5"),那时才走 parse,脏值落 null
final rawRatio = json['ratio'];
ratio = rawRatio is num
? rawRatio.toDouble()
: double.tryParse(rawRatio?.toString() ?? '');
listenPermission = json['listenPermission'];
price = json['price'];
hasBuy = json['hasBuy'];
isFree = json['isFree'];
canPlay = json['canPlay'];
accessType = json['accessType'];
paywall =
json['paywall'] != null ? DramaPaywall.fromJson(json['paywall']) : null;
isActive = json['isActive'];
previewEnabled = json['previewEnabled'];
previewStart = json['previewStart'];
previewSeconds = json['previewSeconds'];
previewVideoUrl = json['previewVideoUrl'];
previewH265Url = json['previewH265Url'];
countBrowse = json['countBrowse'];
countCollect = json['countCollect'];
countComment = json['countComment'];
countDisLike = json['countDisLike'];
countLike = json['countLike'];
countPurchases = json['countPurchases'];
mediaStatus = json['mediaStatus'] != null
? VidStatus.fromMap(json['mediaStatus'])
: null;
createdAt = json['createdAt'];
updateTime = json['updateTime'];
}
/// 只给浏览历史落库用(经 CartoonMediaInfo.toJson → HistoryUtil)。
/// paywall 和 preview* 都是这次请求的临时状态、下次进来要重新拉,故意不存
Map<String, dynamic> toJson() {
return {
'id': id,
'mediaId': mediaId,
'episodeNumber': episodeNumber,
'name': name,
'cover': cover,
'videoUrl': videoUrl,
'h265Url': h265Url,
'audioUrl': audioUrl,
'urlSet': urlSet,
'text': text,
'md5': md5,
'playTime': playTime,
'mediaSize': mediaSize,
'height': height,
'weight': weight,
'ratio': ratio,
'listenPermission': listenPermission,
'price': price,
'hasBuy': hasBuy,
'isFree': isFree,
'canPlay': canPlay,
'accessType': accessType,
'isActive': isActive,
'countBrowse': countBrowse,
'countCollect': countCollect,
'countComment': countComment,
'countDisLike': countDisLike,
'countLike': countLike,
'countPurchases': countPurchases,
if (mediaStatus != null) 'mediaStatus': mediaStatus?.toJson(),
'createdAt': createdAt,
'updateTime': updateTime,
};
}
/// 有声书音频完整地址:相对路径要拼音频 CDN,已是 http(s) 的直接用。
/// 目前有声书播的是 ComicChapterInfo 里的同名实现,这里留着与 audioUrl 字段配套
String get realAudioUrl {
final url = audioUrl;
if (url?.isNotEmpty != true) return '';
if (url!.startsWith('http')) return url;
return path.join(address.Address.audioCdnAddress ?? '', url);
}
}
@@ -0,0 +1,75 @@
class MessageDynamicList {
MessageDynamicList({
this.createdAt,
this.id,
this.sendGender,
this.content,
this.sendUid,
this.objId,
this.sendAvatar,
this.vipLevel,
this.msgType,
this.sendName,
this.objName,
});
MessageDynamicList.fromJson(Map<String, dynamic> json) {
createdAt = json['createdAt'];
id = json['id'];
sendGender = json['sendGender'];
content = json['content'];
sendUid = json['sendUid'];
createdAt = json['createdAt'];
objId = json['objId'];
sendAvatar = json['sendAvatar'];
vipLevel = json['vipLevel'];
msgType = json['msgType'] ?? "";
objName = json['objName'];
isRead = json['isRead'];
superUser = json['superUser'];
awards = List<int>.from((json['awards'] as List?) ?? []);
imgUrl = json['imgUrl'];
vipExpireDateOmitempty = json['vipExpireDateOmitempty'];
objCover = json['objCover'];
sendName = json['sendName'];
likeCount = json['likeCount'];
}
String? id;
int? sendUid;
String? sendName;
String? sendAvatar;
String? sendGender;
int? superUser;
List<int>? awards;
String? msgType;
String? content;
String? imgUrl;
bool? isRead;
String? objId;
String? objName;
String? createdAt;
int? likeCount;
String? vipExpireDateOmitempty;
int? vipLevel;
String? objCover;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['createdAt'] = createdAt;
map['id'] = id;
map['sendGender'] = sendGender;
map['content'] = content;
map['sendUid'] = sendUid;
map['objId'] = objId;
map['sendAvatar'] = sendAvatar;
map['vipLevel'] = vipLevel;
map['msgType'] = msgType;
map['objName'] = objName;
map['isRead'] = isRead;
map['sendName'] = sendName;
return map;
}
}
@@ -0,0 +1,13 @@
/// 积分记录
class CreditRecordModel {
String? desc;
num? integral;
String? createdAt;
CreditRecordModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
desc = json['desc'];
integral = json['integral'];
createdAt = json['createdAt'];
}
}
@@ -0,0 +1,45 @@
/// 单条账单
class BillItemModel {
final String? createdAt;
final String? desc;
final String? tranType;
final int? tranTypeInt;
final int? actualAmount; //实际变动金币
final int? actualIntegral; //实际变动积分
final int? integral;
BillItemModel({
this.createdAt,
this.desc,
this.tranType,
this.tranTypeInt,
this.actualAmount,
this.actualIntegral,
this.integral,
});
/// 数量单位:有积分变动记积分,特定交易类型记次数,其余记金币
String get unit {
if ((integral ?? 0) != 0) return '积分';
if (const {103, 104, 109, 110, 111}.contains(tranTypeInt)) return '';
return '金币';
}
/// 展示用数量:金币和积分只会变动其中一种,同时变动时不展示
int get realCount {
if (actualAmount == 0) return actualIntegral ?? 0;
if (actualIntegral == 0) return actualAmount ?? 0;
return 0;
}
factory BillItemModel.fromJson(Map<String, dynamic> json) => BillItemModel(
createdAt: json["createdAt"],
desc: json["desc"],
tranType: json["tranType"],
tranTypeInt: json["tranTypeInt"],
actualAmount: json["actualAmount"],
actualIntegral: json["actualIntegral"],
integral: json["integral"],
);
}
+172
View File
@@ -0,0 +1,172 @@
/// 代充商人
class PayForModel {
int? imId; //商人在聊天系统中的ID
String? userId; //商人在代充系统中的ID
String? avatar; //头像
String? nickName; //昵称
String? welcomeMsg; //商人欢迎语
List<PayInfoModel>? payInfos; //支持的支付方式
PayForModel();
PayForModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
imId = json['imId'];
userId = json['userId'];
avatar = json['avatar'];
nickName = json['nickName'];
welcomeMsg = json['welcomeMsg'];
payInfos = (json['payInfos'] as List?)?.map((e) => PayInfoModel.fromJson(e)).toList();
}
Map<String, dynamic> toJson() => {
"imId": imId,
"userId": userId,
"avatar": avatar,
"welcomeMsg": welcomeMsg,
"nickName": nickName,
"payInfos": payInfos,
};
PayForModel clone() => PayForModel()
..imId = imId
..userId = userId
..avatar = avatar
..nickName = nickName
..welcomeMsg = welcomeMsg
..payInfos = payInfos?.map((e) => e.clone()).toList() ?? [];
}
/// 一种支付方式
class PayInfoModel {
int? payMethod;
List<int>? payType;
PayInfoModel();
PayInfoModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
payMethod = json['payMethod'];
payType = (json['payType'] as List?)?.cast<int>().toList() ?? [];
}
Map<String, dynamic> toJson() => {
"payMethod": payMethod,
"payType": payType,
};
PayInfoModel clone() => PayInfoModel()
..payMethod = payMethod
..payType = [...?payType];
}
/// 代充配置,整体 base64 后透传给代充 H5
class DCModel {
bool? isReconnect;
/// 商人列表 为空则不展示代充
List<PayForModel>? traders;
String? url;
String? ordUrl;
String? traderUrl;
int? chargeMoney;
///大额支付 小额支付
int? limit;
String? userAgent;
String? wsUrl;
String? picUrl;
/// 商品id,客户端添加,非后端返回,代充值给h5的时候使用
String? productInfo;
DcUserInfo? userInfo;
/// 支付渠道,客户端下单时赋值,非后端返回
String? channel;
DCModel();
DCModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
isReconnect = json['isReconnect'];
//只取第一个商人,后续逻辑都按 traders[0] 走
final trader = (json['traders'] as List?)?.firstOrNull;
if (trader != null) traders = [PayForModel.fromJson(trader)];
url = json['url'];
wsUrl = json['wsUrl'];
picUrl = json['picUrl'];
ordUrl = json['ordUrl'];
traderUrl = json['traderUrl'];
userInfo = DcUserInfo.fromJson(json['userInfo']);
chargeMoney = json['chargeMoney'];
limit = json['limit'];
userAgent = json['userAgent'];
productInfo = json['productInfo'];
}
Map<String, dynamic> toJson() => {
"isReconnect": isReconnect,
"traders": traders,
"url": url,
"ordUrl": ordUrl,
"traderUrl": traderUrl,
"userInfo": userInfo,
"chargeMoney": chargeMoney,
"limit": limit,
"wsUrl": wsUrl,
"picUrl": picUrl,
"userAgent": userAgent,
"productInfo": productInfo,
"channel": channel,
};
DCModel clone() => DCModel()
..isReconnect = isReconnect
..traders = traders?.map((e) => e.clone()).toList() ?? []
..url = url
..ordUrl = ordUrl
..traderUrl = traderUrl
..userInfo = userInfo?.clone()
..chargeMoney = chargeMoney
..limit = limit
..userAgent = userAgent
..wsUrl = wsUrl
..picUrl = picUrl
..productInfo = productInfo
..channel = channel;
}
/// 代充里透传给 H5 的用户信息
class DcUserInfo {
int? uid;
String? gender;
String? name;
String? portrait;
DcUserInfo();
DcUserInfo.fromJson(Map<String, dynamic>? json) {
json ??= {};
uid = json['uid'];
if (json['gender'] is String) gender = json['gender'];
name = json['name'];
portrait = json['portrait'];
}
Map<String, dynamic> toJson() => {
"uid": uid,
"gender": gender,
"name": name,
"portrait": portrait,
};
DcUserInfo clone() => DcUserInfo()
..uid = uid
..gender = gender
..name = name
..portrait = portrait;
}
@@ -0,0 +1,18 @@
import 'package:hgdj/hj_model/mine/exchange/recharge_type_list_model.dart';
import 'dc_model.dart';
/// 可供充值的金币列表
class RechargeListModel {
DCModel? daichong;
List<RechargeTypeModel>? list;
RechargeListModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
daichong = DCModel.fromJson(json['daichong']);
//代充数据挂在上级,逐条塞给档位,档位内部才能拼出代充支付方式
list = (json['list'] as List?)
?.map((e) => RechargeTypeModel.fromJson(e)..daichong = daichong)
.toList();
}
}
@@ -0,0 +1,60 @@
import '../../../hj_page/mine/mine_vip/vip_support_model.dart';
import 'dc_model.dart';
/// 代充的 payMethod → (type, 展示名)
const _dcPayTypes = {
101: ('alipy', '支付寶(人工充值)'),
102: ('wechat', '微信(人工充值)'),
103: ('union', '银联(人工充值)'),
104: ('credit', '信用卡(人工充值)'),
105: ('huabei', '花呗(人工充值)'),
106: ('yunSanPay', '云闪付(人工充值)'),
107: ('qqWallet', 'QQ錢包(人工充值)'),
108: ('jindongPay', '京东支付(人工充值)'),
};
/// 一个充值档位
class RechargeTypeModel {
String? id;
int? amount;
int? money;
String? couponDesc;
DCModel? daichong; // 从上级数据结构手动赋值过来
List<RchgType>? rechargeTypeList;
/// 展示用的支付方式:代充那条要按商人支持的 payMethod 展开成多条
List<RchgType> get rechargeTypeListUI {
final payList = <RchgType>[];
for (final rchg in (rechargeTypeList ?? [])) {
if (rchg.type != 'daichong') {
payList.add(rchg);
continue;
}
final payInfos = daichong?.traders?.firstOrNull?.payInfos ?? [];
for (final info in payInfos) {
final named = _dcPayTypes[info.payMethod];
payList.add(RchgType()
..isOfficial = true
..channel = rchg.channel
..incrAmount = rchg.incrAmount
..incTax = rchg.incTax
..payMethod = info.payMethod
..type = named?.$1
..typeName = named?.$2);
}
}
return payList;
}
//money是分单位 需要/100
int get moneyYuan => ((money ?? 0) / 100).round();
RechargeTypeModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
amount = json['amount'];
money = json['money'];
couponDesc = json['couponDesc'];
rechargeTypeList = (json['rchgType'] as List?)?.map((o) => RchgType.fromJson(o)).toList();
}
}
@@ -0,0 +1,13 @@
/// 下单返回的支付链接
class RechargeUrlModel {
String? payUrl;
/// url-打开外部支付链接;sdk-走 sdk
String? mode;
RechargeUrlModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
payUrl = json['payUrl'];
mode = json['mode'];
}
}
@@ -0,0 +1,25 @@
/// 兑换码记录列表
class ExchangeRecordList {
int? total;
List<ExchangeRecordModel>? data;
ExchangeRecordList.fromJson(Map<String, dynamic>? json) {
json ??= {};
total = json['total'];
data = (json['data'] as List?)?.map((e) => ExchangeRecordModel.fromJson(e)).toList();
}
}
/// 单条兑换记录
class ExchangeRecordModel {
String? code;
String? desc;
String? createdAt;
ExchangeRecordModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
code = json['code'];
desc = json['desc'];
createdAt = json['createdAt'];
}
}
@@ -0,0 +1,31 @@
/// 关注 / 粉丝列表里的用户
class FollowUserModel {
String? id;
int? uid;
String? objcId;
String? name;
String? portrait;
int? fans;
int? totalWorks;
int? videoCount;
bool? hasFollow;
bool? hasCollected;
FollowUserModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
uid = json['uid'];
objcId = json['objcId'];
name = json['name'];
portrait = json['portrait'];
fans = json['fans'];
totalWorks = json['totalWorks'];
videoCount = json['videoCount'];
hasFollow = json['hasFollow'];
hasCollected = json['hasCollected'];
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:hgdj/hj_model/splash/ads_model.dart';
class AdGroup {
final List<AdsInfoModel>? items;
final String? title;
AdGroup({this.title, this.items});
}
class AdTabConfig {
/// 对应着列表数据
final AdGroup? shuAds;
final List<AdsInfoModel>? bannerAds;
/// 对应着网格排版
final AdGroup? hengAds;
AdTabConfig({this.shuAds, this.bannerAds, this.hengAds});
}
class HappyModel {
List<AdsInfoModel>? shuApp;
List<AdsInfoModel>? hengApp;
List<AdsInfoModel>? adv;
List<AdsInfoModel>? gameApp;
List<AdsInfoModel>? ypApp;
List<AdsInfoModel>? zbApp;
List<AdsInfoModel>? qpApp;
HappyModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
shuApp = _ads(json['shuApp']);
hengApp = _ads(json['hengApp']);
adv = _ads(json['adv']);
gameApp = _ads(json['gameApp']);
ypApp = _ads(json['ypApp']);
zbApp = _ads(json['zbApp']);
qpApp = _ads(json['qpApp']);
}
static List<AdsInfoModel>? _ads(dynamic raw) =>
raw is! List ? null : raw.map((e) => AdsInfoModel.fromJson(e)).toList();
}
@@ -0,0 +1,17 @@
/// 官方渠道列表项
class OfficialListItemModel {
String? officialName;
String? officialDesc;
String? officialImg;
String? officialUrl;
num? position;
OfficialListItemModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
officialName = json['officialName'];
officialDesc = json['officialDesc'];
officialImg = json['officialImg'];
officialUrl = json['officialUrl'];
position = json['position'];
}
}
+13
View File
@@ -0,0 +1,13 @@
/// 推广记录里的下线用户
class Promotion {
String? name;
String? portrait;
String? createAt;
Promotion.fromJson(Map<String, dynamic>? json) {
json ??= {};
name = json['name'];
portrait = json['portrait'];
createAt = json['createAt'];
}
}
+66
View File
@@ -0,0 +1,66 @@
/// 积分兑换商品
class IntegralExchangeModel {
String? id;
int? type; //5_实物,兑换后要联系客服填地址
String? img;
int? price;
IntegralExchangeModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
type = json['type'];
img = json['img'];
price = json['price'];
}
}
/// 任务中心:按类型分组的任务列表
class TaskCenterData {
List<DailyTask>? dailyTask; //每日任务
List<DailyTask>? onceTask; //一次性任务
List<DailyTask>? growthTasks; //成长任务
TaskCenterData.fromJson(Map<String, dynamic>? json) {
json ??= {};
dailyTask = _tasks(json['dailyTask']);
onceTask = _tasks(json['onceTask']);
growthTasks = _tasks(json['growthTasks']);
}
static List<DailyTask>? _tasks(dynamic raw) => (raw as List?)?.map((e) => DailyTask.fromJson(e)).toList();
}
/// 单个任务
class DailyTask {
String? id;
String? title;
String? subTitle;
String? desc;
String? img;
String? link;
int? type;
int? prizesIntegral; //完成可得积分
int? countdownType;
String? startAt;
String? endAt;
int? status;
/// 任务所属分组,后端不下发,合并列表时由客户端标记:1_每日 2_一次性 3_成长
int? doType;
DailyTask.fromJson(Map<String, dynamic>? json) {
json ??= {};
id = json['id'];
title = json['title'];
subTitle = json['subTitle'];
desc = json['desc'];
img = json['img'];
link = json['link'];
type = json['type'];
prizesIntegral = json['prizesIntegral'];
countdownType = json['countdownType'];
startAt = json['startAt'];
endAt = json['endAt'];
status = json['status'];
}
}
@@ -0,0 +1,45 @@
/// VIP 卡片统计事件(POST /analytics/events
class VipCardAnalyticsEvent {
final String eventId;
final String eventName;
final String sessionId;
final String occurredAt;
final String? experimentId;
final String? variant;
final String? productId;
final int? price; // 分;下单相关扩展字段,后端可忽略未知字段
VipCardAnalyticsEvent({
required this.eventId,
required this.eventName,
required this.sessionId,
required this.occurredAt,
this.experimentId,
this.variant,
this.productId,
this.price,
});
Map<String, dynamic> toJson() => {
'eventId': eventId,
'eventName': eventName,
'sessionId': sessionId,
'occurredAt': occurredAt,
if (experimentId != null) 'experimentId': experimentId,
if (variant != null) 'variant': variant,
if (productId != null) 'productId': productId,
if (price != null) 'price': price,
};
}
/// 事件名
abstract class VipCardAnalyticsEventName {
/// 卡皮页展示(A/B
static const pageView = 'VIP_CARD_PAGE_VIEW';
/// 套餐曝光(选中 / 默认选中)
static const productImpression = 'VIP_PRODUCT_IMPRESSION';
/// 无购买直接关闭
static const closeWithoutPurchase = 'VIP_CARD_CLOSE_WITHOUT_PURCHASE';
}
+121
View File
@@ -0,0 +1,121 @@
import '../../tools_base/banner/ads_grid_view_widget.dart';
/// 广告信息
class AdsInfoModel {
// ----- 基础信息 -----
String? id;
String? title;
String? cover; //封面图
String? href; //跳转地址
String? newUrl;
// ----- 官方/来源信息 -----
String? officialImg;
String? officialName;
String? officialDesc;
String? officialUrl;
String? officialType;
// ----- 展示位置与排序 -----
String? positionName;
String? imPosition; //IM 广告位标识(IM_LIST_BANNER 等字符串 key)
/// 广告位置
/// 0 - 启动页广告
/// 1 - 首页-推荐显示小广告
/// 2 - 首页-推荐公告展示
/// 3 - 消息界面广告
/// 4 - 首页-热点 banner广告
int? position;
/// 同一位置使用此进行排序
int? sortCode;
/// 广告样式
/// full-screen 全屏(启动页、短视频)
/// banner-720300 Banner720300
/// banner-720150 Banner720150
/// banner-750700 Banner750700
/// banner-800450 Banner800450
/// pop-ups-600800 弹窗600800
/// pop-ups-512512 弹窗512512
/// small-cube 小方块
/// vertical-cube 竖方块
String? coverImgSize;
// ----- 业务控制 -----
int? moduleType; //楼凤广告模块过滤
int? watchTime; //广告关闭时长
AdsInfoModel({this.id, this.cover, this.href, this.title});
/// 是否有广告倒计时
bool get hasAdTime => (watchTime ?? 0) > 0;
/// 根据 coverImgSize 映射广告布局样式(small-cube 与未知值同走默认)
AdStyle get adStyle => switch (coverImgSize) {
'vertical-cube' => AdStyle.oneScrollBig,
'banner-720300' => AdStyle.banner,
_ => AdStyle.twoAutoScroll,
};
//同时兼容 Adv(image/name/url) 与 ShuApp(icon/name/url) 字段
AdsInfoModel.fromJson(dynamic json) {
json ??= {};
id = json['id'];
title = json['title'] ?? json['name'];
cover = json['cover'] ?? json['image'] ?? json['icon'];
href = json['href'] ?? json['url'];
newUrl = json['newUrl'];
officialImg = json['officialImg'];
officialName = json['officialName'];
officialDesc = json['officialDesc'];
officialUrl = json['officialUrl'];
officialType = json['officialType'];
positionName = json['positionName'];
position = json['position'];
sortCode = json['sortCode'];
coverImgSize = json['coverImgSize'];
moduleType = json['moduleType'];
watchTime = json['watchTime'];
}
//和 fromJson 同一套解析,别再各写一份
static AdsInfoModel fromMap(Map<dynamic, dynamic>? map) => AdsInfoModel.fromJson(map);
Map toJson() => {
"id": id,
"title": title,
"cover": cover,
"href": href,
"position": position,
"sortCode": sortCode,
"positionName": positionName,
};
}
/// 公告信息
class AnnounceInfoBean {
String? content;
String? cover;
String? href; //图片公告点击时跳转的地址
int? type; //0:系统公告 1:图片公告 3:活动公告
String? title;
static AnnounceInfoBean fromJson(Map<String, dynamic>? map) {
map ??= {};
return AnnounceInfoBean()
..content = map['content']
..cover = map['cover']
..href = map['href']
..type = map['type']
..title = map['title'];
}
Map toJson() => {
"content": content,
"cover": cover,
"href": href,
"type": type,
"title": title,
};
}
+23
View File
@@ -0,0 +1,23 @@
class BannerJumpEntity {
String? id;
int? position;
String? url;
String? title;
String? startAt;
String? banner;
String? endAt;
int? countdownType;
BannerJumpEntity({this.title, this.id, this.position, this.url, this.startAt, this.banner, this.endAt, this.countdownType});
BannerJumpEntity.fromJson(dynamic json) {
id = json['id'];
position = json['position'];
url = json['url'];
title = json['title'];
startAt = json['startAt'];
endAt = json['endAt'];
banner = json['banner'];
countdownType = json['countdownType'];
}
}
+24
View File
@@ -0,0 +1,24 @@
/// 粘贴版信息
class CutInfo {
// pc是邀请码promoteCode
String? pc;
/// dc是渠道 distinctCode
String? dc;
String? tid;
static CutInfo fromMap(Map<String, dynamic>? map) {
map ??= {};
return CutInfo()
..pc = map['pc']
..dc = map['dc']
..tid = map['tid'];
}
Map toJson() => {
"pc": pc,
"dc": dc,
"tid": tid,
}..removeWhere((key, value) => value == null || (value as String?)?.isEmpty == true);
}
@@ -0,0 +1,605 @@
import 'package:hgdj/assets_tool/images.dart';
import '../../alert/vip_guide/guide_manager.dart';
import '../../config/config.dart';
import '../../hj_page/ai/ai_sub_type/ai_function_logic.dart';
import '../../hj_page/main_page/provider/msg_provider.dart';
import '../../hj_page/pre_sale/pre_sale_model.dart';
import 'ads_model.dart';
/// 预售/活动状态
class AdvanceStatus {
bool? activityPopUp; //预售活动是否首页弹窗
bool? activityStatus; //是否开启
bool? balancePayment; //是否有尾款支付
String? oid;
PrivilegeLimit? privilegeLimit; //预付权益
AdvanceStatus.fromJson(dynamic json) {
activityStatus = json['activityStatus'];
balancePayment = json['balancePayment'];
activityPopUp = json['activityPopUp'];
oid = json['oid'];
privilegeLimit = json['privilegeLimit'] != null
? PrivilegeLimit.fromJson(json['privilegeLimit'])
: null;
}
}
/// 预付权益限制与剩余次数
class PrivilegeLimit {
bool? hasLimit; //是否有权益限制
Remain? remain;
PrivilegeLimit({this.hasLimit, this.remain});
PrivilegeLimit.fromJson(Map<String, dynamic> json) {
hasLimit = json['hasLimit'];
remain = json['remain'] != null ? Remain.fromJson(json['remain']) : null;
}
}
/// 今日各项功能剩余次数
class Remain {
int? todayAiUndressCount; //今日剩余ai脱衣次数
int? todayCoinVideoCount; //今日剩余金币视频观看次数
int? todayDownloadCount; //今日剩余下载次数
Remain(
{this.todayAiUndressCount,
this.todayCoinVideoCount,
this.todayDownloadCount});
Remain.fromJson(Map<String, dynamic> json) {
todayAiUndressCount = json['todayAiUndressCount'];
todayCoinVideoCount = json['todayCoinVideoCount'];
todayDownloadCount = json['todayDownloadCount'];
}
void reduceVideoCoinCount() {
todayCoinVideoCount = (todayCoinVideoCount ?? 0) - 1;
}
}
/// 同一份 bannerJump 下按位置取:key '1' 播放页,'2' 全民代理页
class BannerJump {
AdditionalProp? additionalProp;
BannerJump({this.additionalProp});
BannerJump.fromJson(Map<String, dynamic> json, {String key = '1'}) {
additionalProp =
json[key] != null ? AdditionalProp.fromJson(json[key]) : null;
}
}
class AdditionalProp {
String? id;
String? title; //标题
String? banner; //BANNER图片
String? url; //跳转地址
int? position; //位置 1:视频播放器下方
String? startAt; //开始时间
String? endAt; //结束时间
int? countdownType;
AdditionalProp(
{this.id,
this.banner,
this.endAt,
this.position,
this.startAt,
this.title,
this.url,
this.countdownType});
AdditionalProp.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
banner = json['banner'];
url = json['url'];
position = json['position'];
startAt = json['startAt'];
endAt = json['endAt'];
countdownType = json['countdownType'];
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'banner': banner,
'url': url,
'position': position,
'startAt': startAt,
'endAt': endAt,
'countdownType': countdownType,
};
}
/// 全局配置:域名、广告、AI价格、活动、搜索等
class DomainSourceModel {
// ----- 域名 / 线路 / 版本 -----
List<String>? domain; //接口域名
List<Sourcelist>? sourceList;
List<CheckVersionInfo>? ver;
// ----- 广告 / 公告 -----
AdsBean? ads;
// ----- AI 功能价格 -----
String? aiUndressPrice; //AI脱衣价格
String? aiImageToVideoPrice; //图生视频价格
String? aiTextToImagePrice; //绘图价格
String? aiTextToNovelPrice; //小说价格
// ----- 活动 / 预售 / Banner -----
AdvanceStatus? advanceStatus;
AdvancePage? advancePage;
BannerJump? bannerJump; //限时Banner活动
BannerJump? proxyBannerJump;
// ----- 抽奖 -----
String? luckyDrawH5; //抽奖H5链接
String? luckyDrawIcon; //抽奖入口浮动图标
// ----- 搜索 -----
List<String>? hotSearchTerms; //搜索热门词
List<String>? searchHintWord; //搜索提示词
// ----- 暗网 VIP -----
String? darkWebVipId; //暗网vip卡id
String? darkWebVipName; //暗网vip卡名
String? shortDramaCardId; //短剧卡id(付费墙默认选中的会员卡)
List<String>? recommendVipIds;
// ----- 其他 -----
bool? broadcast; //是否展示直播模块
bool? storeIsOpen; //原味商店开关
List<JGAreaModel>? jgArea; //金刚区配置
static DomainSourceModel fromJson(Map<String, dynamic>? map) {
map ??= {};
final info = DomainSourceModel();
// 直接写入全局 Config 的配置项
Config.vipMark = map['vipMark'] ?? true;
Config.coinMark = map['coinMark'] ?? true;
Config.freeMark = map['freeMark'] ?? true;
Config.signIcon = map['signIcon'];
Config.darkWebEnable = map['darkWebEnable'] ?? true;
Config.darkWebImg = map['darkWebImg'];
Config.darkWebIcon = map['darkWebIcon'];
Config.darkWebIconName = map['darkWebIconName'];
Config.mineBg = map['personalCenterBackground'];
Config.entryPopupEnabled =
map['shortDramaEntryPopupEnabled'] ?? true; //短剧引导气泡,字段缺失当开启
MineMsgProvider().payTier =
PayTierModel.fromJson(map); //支付状态分层(各展示位图片/卡ID/倒计时)
GuideManager().update(map['paymentGuide']); //付费引导各场景开关(弹窗一律读缓存,不再临时查接口)
if (map['aiSwitchConf'] is List) {
Config.aiTypes = (map['aiSwitchConf'] as List)
.map((e) => AISwitchConf.fromJson(e))
.toList();
}
if (map.containsKey('bannerJumpList')) {
Config.bannerJumps = (map['bannerJumpList'] as List?)
?.map((o) => BannerJumpEntity.fromJson(o))
.toList() ??
[];
}
// 域名 / 线路 / 版本
if (map.containsKey('domain')) {
info.domain = (map['domain'] as List?)?.map((o) => o.toString()).toList();
}
if (map.containsKey('sourceList')) {
info.sourceList = (map['sourceList'] as List?)
?.map((o) => Sourcelist.fromJson(o))
.toList();
}
if (map.containsKey('ver')) {
info.ver = (map['ver'] as List?)
?.map((o) => CheckVersionInfo.fromMap(o))
.toList();
}
// 广告
if (map.containsKey('ads')) {
info.ads = AdsBean.fromMap(map['ads']);
}
// AI 价格
info.aiUndressPrice = map['aiUndressPrice']?.toString();
info.aiImageToVideoPrice = map['aiImageToVideoPrice']?.toString();
info.aiTextToImagePrice = map['aiTextToImagePrice']?.toString();
info.aiTextToNovelPrice = map['aiTextToNovelPrice']?.toString();
// 活动 / 预售 / Banner
if (map.containsKey('advancePage')) {
info.advancePage = AdvancePage.fromJson(map['advancePage']);
}
if (map.containsKey('advanceStatus')) {
info.advanceStatus = AdvanceStatus.fromJson(map['advanceStatus']);
}
info.bannerJump = map['bannerJump'] != null
? BannerJump.fromJson(map['bannerJump'])
: null;
info.proxyBannerJump = map['bannerJump'] != null
? BannerJump.fromJson(map['bannerJump'], key: '2')
: null;
// 抽奖
info.luckyDrawH5 = map['luckyDrawH5'];
info.luckyDrawIcon = map['luckyDrawIcon'];
// 搜索
if (map['hotSearchTerms'] is List) {
info.hotSearchTerms =
(map['hotSearchTerms'] as List).map((e) => e.toString()).toList();
}
if (map['searchHintWord'] is List) {
info.searchHintWord =
(map['searchHintWord'] as List).map((e) => e.toString()).toList();
}
// 暗网 VIP
info.darkWebVipId = map['darkWebVipId'];
info.darkWebVipName = map['darkWebVipName'];
info.shortDramaCardId = map['shortDramaCardId'];
if (map['recommendVipIds'] is List) {
info.recommendVipIds =
(map['recommendVipIds'] as List).map((e) => e.toString()).toList();
}
// 其他
info.broadcast = map['broadcast'] ?? false;
info.storeIsOpen = map['storeIsOpen'];
if (map['jgArea'] is List) {
info.jgArea = (map['jgArea'] as List?)
?.map((e) => JGAreaModel.fromJson(e))
.toList();
}
return info;
}
}
/// 跑马灯公告
class MarqueeModel {
String? content; //跑马灯内容
String? url; //跳转连接
bool? active; //激活状态
String? id; //公告id
int? type; //跑马灯类型,0:会员中心
int? position;
String? positionDesc;
static MarqueeModel fromMap(Map<String, dynamic>? map) {
map ??= {};
return MarqueeModel()
..content = map['content']
..url = map['url']
..active = map['active']
..id = map['id']
..type = map['type']
..position = map['position']
..positionDesc = map['positionDesc'];
}
}
/// 版本检测信息
class CheckVersionInfo {
String? description;
bool? forcedUpdate; //是否强制更新
String? platform;
String? url; //Android 的 apk 下载地址
String? iosUrl; //iOS 的 App Store 地址,点升级直接跳商店
String? verName;
int? code;
static CheckVersionInfo fromMap(Map<String, dynamic>? map) {
map ??= {};
return CheckVersionInfo()
..description = map['description']
..forcedUpdate = map['forcedUpdate']
..platform = map['platform']
..url = map['url']
..iosUrl = map['iosUrl']
..verName = map['verName']
..code = map['Code'];
}
}
/// 广告聚合:图片广告列表 + 文本公告
class AdsBean {
List<AdsInfoModel>? adsInfoList;
List<AnnounceInfoBean>? announInfo; //文本公告
///多个公告
List<AnnounceInfoBean>? announList;
static AdsBean fromMap(Map<String, dynamic>? map) {
map ??= {};
return AdsBean()
..announInfo = (map['announInfo'] as List?)
?.map((o) => AnnounceInfoBean.fromJson(o))
.toList()
..announList = (map['announList'] as List?)
?.map((o) => AnnounceInfoBean.fromJson(o))
.toList()
..adsInfoList = (map['adsInfoList'] as List?)
?.map((o) => AdsInfoModel.fromMap(o))
.toList();
}
Map toJson() => {
"announInfo": announInfo,
};
}
/// 金刚区单项
class JGAreaModel {
String? id;
String? name;
int? type;
String? img;
String? desc;
int? linkType;
String? linkUrl;
String? mid;
JGAreaModel(
{this.id,
this.name,
this.img,
this.desc,
this.linkType,
this.linkUrl,
this.type,
this.mid});
factory JGAreaModel.fromJson(Map<String, dynamic> json) => JGAreaModel(
id: json["id"],
name: json["name"],
img: json["img"],
desc: json["desc"],
linkType: json["link_type"],
linkUrl: json["link_url"],
type: json["type"],
mid: json["mid"],
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"img": img,
"desc": desc,
"link_type": linkType,
"link_url": linkUrl,
'mid': mid,
};
}
/// 线路源
class Sourcelist {
String? id;
List<Domain>? domain;
bool? isActive;
String? type;
Sourcelist({this.id, this.domain, this.isActive, this.type});
factory Sourcelist.fromJson(Map<String, dynamic> json) => Sourcelist(
id: json["id"],
domain:
List<Domain>.from(json["domain"].map((x) => Domain.fromJson(x))),
isActive: json["isActive"],
type: json["type"],
);
Map<String, dynamic> toJson() => {
"id": id,
"domain": List<dynamic>.from(domain!.map((x) => x.toJson())),
"isActive": isActive,
"type": type,
};
}
/// 域名节点(带权重)
class Domain {
int? weight;
String? url;
String? desc;
int? status;
Domain({this.weight, this.url, this.desc, this.status});
factory Domain.fromJson(Map<String, dynamic> json) => Domain(
weight: json["weight"],
url: json["url"],
desc: json["desc"],
status: json["status"],
);
Map<String, dynamic> toJson() => {
"weight": weight,
"url": url,
"desc": desc,
"status": status,
};
}
//AI 类型配置表:type → 名称/功能枚举/图标(存图标名,读取时拼 .aiPath)
class _AiTypeConf {
final String name;
final AiType type;
final String img;
const _AiTypeConf(this.name, this.type, this.img);
}
const _aiTypeConfMap = <int, _AiTypeConf>{
1: _AiTypeConf('AI脱衣', AiType.autoStrip, 'ai_address.png'),
2: _AiTypeConf('视频换脸', AiType.videoChangeFace, 'ai_video_face.png'),
3: _AiTypeConf('图片换脸', AiType.imageChangeFace, 'ai_pic_face.png'),
4: _AiTypeConf('图生视频', AiType.imageToVideo, 'ai_itv.png'),
5: _AiTypeConf('AI绘画', AiType.aiPaint, 'ai_draw.png'),
6: _AiTypeConf('AI小说', AiType.aiNovel, 'ai_novel_enter.png'),
7: _AiTypeConf('AI女友', AiType.aiMate, 'ai_mate_enter.png'),
};
/// AI 功能开关配置
class AISwitchConf {
int? type; //AI类型 1:AI脱衣 2:AI视频换脸 3:AI图片换脸 4:图生视频 5:文生图 6:AI小说
int? sort; //排序,越小越靠前
bool? isOpen; //是否开启
String? aiTypeName;
String? img; //图标(选中/未选中同一张,渲染时统一染白)
AiType? aiType;
AISwitchConf.fromJson(Map<String, dynamic> json) {
type = json['type'];
sort = json['sort'];
isOpen = json['isOpen'];
final conf = _aiTypeConfMap[type];
if (conf != null) {
aiTypeName = conf.name;
aiType = conf.type;
img = conf.img.aiPath;
}
}
}
/// 限时 Banner 跳转配置
class BannerJumpEntity {
String? id;
int? position;
String? url;
String? title;
String? startAt;
String? banner;
String? endAt;
int? countdownType;
BannerJumpEntity(
{this.title,
this.id,
this.position,
this.url,
this.startAt,
this.banner,
this.endAt,
this.countdownType});
BannerJumpEntity.fromJson(dynamic json) {
id = json['id'];
position = json['position'];
url = json['url'];
title = json['title'];
startAt = json['startAt'];
endAt = json['endAt'];
banner = json['banner'];
countdownType = json['countdownType'];
}
}
/// 支付状态分层(ping/domain 的 paymentStatusPopup 字段),用于付费流程的用户分层
enum PayTier {
newUnpay('new_unpay'), //新用户:注册≤1天且从未付费 → 推新人200卡(新人价+首购引导+紧迫感)
under7DayUnpay('under_7_day_unpay'), //未付费·注册1~7天 → 推300卡(限时优惠+紧迫感)
over7DayUnpay('over_7_day_unpay'), //未付费·注册>7天 → 推300卡(永久卡唤醒需求+信任背书)
over7DayNeedUpgrade(
'over_7_day_need_upgrade'), //低级会员需续费:注册>7天且已付费、未达最高等级(<5) → 推500至尊卡
normal('normal'), //正常用户:无特殊弹窗
unregistered('unregistered'); //未注册(未登录)
final String value;
const PayTier(this.value);
static PayTier? from(String? value) {
if (value == null || value.isEmpty) return null;
for (final e in PayTier.values) {
if (e.value == value) return e;
}
return null;
}
}
/// 支付状态弹窗配置(收敛 ping/domain 支付分层相关字段)
class PayTierModel {
PayTier? status; //用户分层
PayTierConfig? config; //各展示位配置(图片/卡ID/倒计时)
PayTierModel({this.status, this.config});
PayTierModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
status = PayTier.from(json['paymentStatusPopup']);
// /ping/domain 配置在 paymentStatusPopupConfig 下;/ping/domain/refresh 平铺在外层,回退用 json 本身
config = PayTierConfig.fromJson(json['paymentStatusPopupConfig'] ?? json);
}
}
/// 支付状态弹窗各展示位配置(meTab/playPage 为图片链接;vipCard 卡IDlastDiscountTime 优惠截止时间)
/// 全站新人卡/分层优惠倒计时的唯一数据源,各展示位一律读这里,不再另开接口取时间
class PayTierConfig {
String? homePage; //首页弹窗图片链接
String? homePageFlot; //首页浮窗图片链接
String? playPage; //播放页图片链接
String? meTab; //me tab 图片链接
String? vipCard; //点击跳转的会员卡 ID
int? lastDiscountTime; //优惠截止 Unix 时间戳(秒),早于当前则不展示倒计时
PayTierConfig(
{this.homePage,
this.homePageFlot,
this.playPage,
this.meTab,
this.vipCard,
this.lastDiscountTime});
PayTierConfig.fromJson(Map<String, dynamic>? json) {
json ??= {};
homePage = json['homepage'];
homePageFlot = json['homepageFlot'];
playPage = json['playPage'];
meTab = json['meTab'];
vipCard = json['vipCard'];
lastDiscountTime = _parseEpochSec(json['lastDiscountTime']);
}
static const _minValidEpochSec =
946684800; //2000-01-01,用于挡掉后端零值时间 0001-01-01T00:00:00Z
//兼容 Unix 秒(int) / 数字字符串 / ISO 字符串(带 Z 或 +08:00 时区);零值哨兵一律归 null,避免负 epoch 流入业务
static int? _parseEpochSec(dynamic v) {
if (v == null) return null;
int? sec;
if (v is num) {
sec = v.toInt();
} else if (v is String) {
final s = v.trim();
if (s.isEmpty) return null;
sec = int.tryParse(s) ??
(DateTime.tryParse(s)?.millisecondsSinceEpoch ?? 0) ~/ 1000;
}
return (sec != null && sec >= _minValidEpochSec) ? sec : null;
}
/// 优惠剩余秒数(截止时刻 - 当前),≤0 表示已过期/不展示倒计时
int get discountRemainSec {
final end = lastDiscountTime ?? 0;
if (end <= 0) return 0;
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final r = end - now;
return r > 0 ? r : 0;
}
bool get hasDiscountCountdown => discountRemainSec > 0;
String get discountHour =>
(discountRemainSec ~/ 3600).toString().padLeft(2, '0');
String get discountMin =>
((discountRemainSec ~/ 60) % 60).toString().padLeft(2, '0');
String get discountSec => (discountRemainSec % 60).toString().padLeft(2, '0');
}
@@ -0,0 +1,21 @@
class WatchCount {
int? watchCount; // 剩余免费观看次数
int? total; // 免费观看总次数(后端下发,用于"剩余 x/y 次"展示)
bool? isCan; // 金币视频不再免费次数内
//⚠️注意: coinWatchCount: 活动权益金币视频免费观看次数, 需要手动赋值,
int coinWatchCount = -1; //短视频业务专用, 因为短视频业务提前预加载数据,需要copy对象,保存当前对应的count值
static WatchCount fromJson(Map<String, dynamic>? map) {
map ??= {};
return WatchCount()
..watchCount = map['watchCount']
..total = map['total']
..isCan = map['isCan'];
}
//只复制展示用的次数;coinWatchCount 按注释由调用方自行赋值
WatchCount copy() => WatchCount()
..watchCount = watchCount
..total = total;
}
@@ -0,0 +1,47 @@
/// 推广收益概览
class UserIncomeModel {
String? totalAmount; // 可提现收益
String? totalIncomeAmount; // 累计收益总额
String? totalPayUserCount; // 累计付费用户
String? totalInviteUserCount; // 累计推广用户
String? todayInviteUserCount; // 今日推广用户
String? monthInviteUserCount; // 当月推广用户
String? monthIncomeAmount; // 当月推广收益金币
String? todayIncomeAmount; // 今日推广收益金币
UserIncomeModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
totalAmount = json['totalAmount'];
totalIncomeAmount = json['totalIncomeAmount'];
totalPayUserCount = json['totalPayUserCount'];
totalInviteUserCount = json['totalInviteUserCount'];
todayInviteUserCount = json['todayInviteUserCount'];
monthInviteUserCount = json['monthInviteUserCount'];
monthIncomeAmount = json['monthIncomeAmount'];
todayIncomeAmount = json['todayIncomeAmount'];
}
}
/// 推广收益列表(分页)
class InviteIncomeModel {
bool? hasNext;
List<InviteItem>? items;
InviteIncomeModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
hasNext = json['hasNext'];
items = (json['list'] as List?)?.map((e) => InviteItem.fromJson(e)).toList();
}
}
/// 单条推广记录
class InviteItem {
String? userName; // 被邀请人昵称
int? incomeAmount; // 该用户带来的收益金币
InviteItem.fromJson(Map<String, dynamic>? json) {
json ??= {};
userName = json['userName'];
incomeAmount = json['incomeAmount'];
}
}
+139
View File
@@ -0,0 +1,139 @@
import 'package:hgdj/assets_tool/images.dart';
/// 用户信息
class UserInfoModel {
// —— 基础资料 ——
int? uid; // 用户ID
String? name; // 昵称
String? portrait; // 头像
String? gender; // 性别
String? summary; // 个性签名
String? mobile; // 手机号
int? urrPortraitStatus; // 头像审核状态
// —— 账号 / 登录 ——
String? token; // 登录 token
String? devID; // 设备 ID
String? createdAt; // 注册时间
String? districtCode; // 渠道码
// —— 社交统计 ——
int? fans; // 粉丝数
int? likeCount; // 获赞数
bool? isFollow; // 是否已关注
// —— 会员 / VIP ——
bool? isVip; // 是否会员
int? vipLevel; // 会员等级 1-影片会员 2-ACG会员 3-超级会员(一卡通)
String? vipName; // 会员名称
String? vipExpireDate; // 会员到期时间
bool? isUpgrade; // 是否可升级
// —— 推广 / 邀请 ——
String? promoteURL; // 推广链接
String? promotionCode; // 我的邀请码
String? inviterCode; // 邀请人邀请码
// —— 金币 / 观影券 / 权益 ——
bool allGoldVideoFree = false; // 是否全场金币视频免费
String? goldVideoFreeExpire; // 金币视频免费权益到期时间
List<MyCoupon>? goldVideoCoupon; // 观影券列表
String? broadcastExpire; // 直播观看权益有效期
String? dramaExpire; // 短剧卡到期时间,晚于当前时间就是有短剧权益
int? payVidDiscount; // 付费视频折扣
// —— 创作 / 等级 ——
int? vidUploadCount; // 视频上传数
int? coverUploadCount; // 封面上传数
int? level; // 当前等级
// —— 状态 / 权限 ——
int? adverAbTestShowType; // 广告AB -1-首次进app免广告 0-全部展示 其他N-注册后N分钟内免广告(分层后固定不变)
// ============ 计算属性 ============
/// 观影券总数量
int get couponCount =>
(goldVideoCoupon ?? []).fold(0, (sum, e) => sum + (e.count ?? 0));
/// 可解锁该金币视频的观影券面额:券面额需 >= 视频金币,都不够就退回第一张
int? couponGold(int? originCoins) {
final coupons = goldVideoCoupon ?? [];
for (final coupon in coupons) {
if ((coupon.gold ?? 0) >= (originCoins ?? 0)) return coupon.gold ?? 0;
}
return coupons.isEmpty ? null : coupons.first.gold;
}
/// 作品总数(视频上传 + 封面上传)
int get totalWorkCount => (vidUploadCount ?? 0) + (coverUploadCount ?? 0);
/// 会员等级对应的角标图片;等级 1~4,0 级(含推广会员)无角标
String get vipImageName {
final level = vipLevel ?? 0;
if (level < 1) return "";
return "vip_${level.clamp(1, 4)}.webp".mineImgPath;
}
/// 是否充值会员
bool get isRechargeVIP => (isVip ?? false) && (vipLevel ?? 0) > 0;
// ============ 序列化 ============
UserInfoModel.fromJson(Map<String, dynamic>? map) {
map ??= {};
uid = map['uid'];
name = map['name'];
portrait = map['portrait'];
gender = map['gender'];
summary = map['summary'];
mobile = map['mobile'];
urrPortraitStatus = map['urrPortraitStatus'];
token = map['token'];
devID = map['devID'];
createdAt = map['createdAt'];
districtCode = map['districtCode'];
fans = map['fans'];
likeCount = map['likeCount'];
isFollow = map['isFollow'];
isVip = map['isVip'];
vipLevel = map['vipLevel'];
vipName = map['vipName'];
vipExpireDate = map['vipExpireDate'];
isUpgrade = map['isUpgrade'];
promoteURL = map['promoteURL'];
promotionCode = map['promotionCode'];
inviterCode = map['inviterCode'];
allGoldVideoFree = map['allGoldVideoFree'] ?? false;
goldVideoFreeExpire = map['goldVideoFreeExpire'];
goldVideoCoupon = (map['goldVideoCoupon'] as List?)
?.map((o) => MyCoupon.fromJson(o))
.toList();
broadcastExpire = map['broadcastExpire'];
dramaExpire = map['dramaExpire'];
payVidDiscount = map['payVidDiscount'];
vidUploadCount = map['vidUploadCount'];
coverUploadCount = map['coverUploadCount'];
level = map['level'];
adverAbTestShowType = map['adverAbTestShowType'];
}
}
/// 观影券
class MyCoupon {
int? gold; // 券面额(金币)
int? count; // 数量
MyCoupon.fromJson(Map<String, dynamic>? map) {
map ??= {};
gold = map['gold'];
count = map['count'];
}
}
+35
View File
@@ -0,0 +1,35 @@
/// 钱包信息,来自 /mine/wallet
class WalletModel {
// ===== 收益 =====
int? income; //收益金币的整数部分
double? incomePot; //收益金币的零头,和 income 一起算总收益
num? vidIncome; //视频收益
// ===== 余额 =====
int? amount; //充值金币余额,购买后会被写回
int? integral; //积分
// ===== 剩余次数 =====
int? downloadCount; //视频下载次数
int? aiUndressFreeTimes; //AI 脱衣免费次数
int? todayAiFreeTimes; //今日 AI 免费次数
//字段顺序与上方声明一致,便于比对
WalletModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
income = json['income'];
//后端可能下发 double / int / 数字字符串,统一按字符串解析。
//解析不了、或拿到 NaN/Infinity 都留 null 当没零头——balance 的 `~/` 吃到它们会抛 UnsupportedError
final pot = double.tryParse('${json['incomePot'] ?? 0}');
incomePot = pot != null && pot.isFinite ? pot : null;
vidIncome = json['vidIncome'];
amount = json['amount'];
integral = json['integral'];
downloadCount = json['downloadCount'];
aiUndressFreeTimes = json['aiUndressFreeTimes'];
todayAiFreeTimes = json['todayAiFreeTimes'];
}
//收益总额:先放大 100 倍算再取整,避免 double 直接相加的精度误差
int get balance => ((income ?? 0) * 100.0 + (incomePot ?? 0) * 100) ~/ 100;
}
+669
View File
@@ -0,0 +1,669 @@
import 'package:hgdj/hj_model/splash/ads_model.dart';
import 'package:hgdj/hj_utils/date_time_util.dart';
import '../config/address.dart';
import '../tools_base/global_store/store.dart';
import 'cartoon_media_info.dart';
import 'drama_media_info.dart';
import 'media_content.dart';
/// 通用视频/帖子/图集/动漫/广告等多业务复合模型
/// 后端用同一个 model 承载多种业务,按 [newsType]、[mediaType]、[videoType] 区分具体形态
class VideoModel {
// ===== 标识 / 类型 =====
String? id; // 视频/帖子ID
String? vid; // 视频 vid
String? subid; // 动漫集数id
String? lsjId; // 老司机媒体资源ID
int? uid; // 发布者UID
String? newsType; // 帖子类型 SP/CSP/COVER_SP/COVER/AD_SP/AD_COVER
int? type; // 类型
int? videoType; // 0视频, 1 动漫
String? mediaType; // 媒体子类型:'image' 表示漫画(单位为"话"),其他视为视频/动漫(单位为"集")
int? showType; // 0-所有的人都可以看 1-奇数可看 2-偶数可看
//0 未审核 1通过 2审核失败 3视为免费 5、已下架
int? status;
String? reason; //审核拒绝理由
// ===== 内容 / 展示 =====
String? title; // 标题
String? name; // 动漫番号
String? desc; // 描述
String? content; // 正文内容
String? richText; //图文html
String? linkStr; // 富文本
String? cover; // 封面
String? coverThumb; // 封面缩略图
List<String>? seriesCover; // 多封面列表
List<TagsBean>? tags; // 标签列表
String? via; // 来源渠道
String? createdAt; // 创建时间
String? reviewAt; // 审核通过时间
bool? isTopping; // 是否置顶
bool? chosen; // 置精
Publisher? publisher; // 发布者
CommentBean? comment; // 热评
// ===== 播放源 =====
String? sourceURL; // 源视频地址(H.264,兜底)
String? h265Url; // H.265 播放地址(可空/空串表示无 265 资源)
String? chineseLink; // 中文字幕
String? codelessLink; // 无码链接 马赛克
String? previewURL; // 预览片段地址
int playContentStyle = 1; // 1:源视频,2:中文字幕,3:破解版(无码)
int? playTime; // 时长(秒)
double? ratio; //视频宽高比,没返回或者0,强制设置16/9
String? resolution; // 分辨率
int? size; // 文件大小
int? downloadAllow; // 0表示不允许下载 1表示VIP 2表示免费
// ===== 计价 / 权限 =====
int? coins; // 折扣价格
int? originCoins; // 原价
int? permission; //动漫权限 0:会员 1:金币购买 2:免费
///是否免费观看
bool? freeArea;
int? freeTime; // 免费试看时长(秒)
int? previewStart; //预览时间起始
/// 是否展示「免费试看」角标(需同时满足 ping 的 freeMark 总开关)
bool? showFreeTrialBadge;
/// 剩余试看次数
int? freeTrialRemaining;
/// 当前视频是否可用免费试看
bool? canUseFreeTrial;
VidStatus? vidStatus; // 当前用户对该视频的状态
// ===== 统计 =====
int? hot; // 热度
int? likeCount; // 点赞数
int? collectCount; // 收藏数
int? commentCount; // 评论数
int? playCount; // 播放数
int? pageViewCount; // 浏览数
int? videoCount; // 视频数
int? purchaseCount; // 购买数
// ===== 种子 =====
String? seedLink; // 种子链接
String? seedLinkUrl; // 种子下载链接
int? seedSize; //种子大小
int? seedPlayTime; // 种子时长(秒)
// ===== 动漫 / 漫画 =====
CartoonMediaInfo? mediaInfo; // 动漫信息
int? totalEpisode; // 动漫集数
int? updateStatus; //更新状态 0 默认(已完结) 1、更新中 2、已完结
// ===== 短剧 =====
// 只由 DramaMediaInfo.toVideoModel 组装,不参与 fromJson——短剧走自己的接口,不从视频列表里解析
DramaMediaInfo? dramaInfo; // 所属短剧(剧名/总集数/分集列表)
MediaContent? dramaEpisode; // 当前分集(权限/付费墙/播放地址以它为准)
int? episodeNo; // 当前是第几集
// ===== 广告 =====
AdsInfoModel? randomAdsInfo; // 随机广告
List<AdsInfoModel>? adsInfoArr; // 广告列表
/// 广告链接
String? linkUrl;
// ===== 客户端本地态(不参与接口解析)=====
// 短视频业务用到,二级短视频列表数据没有获取权限,需要通过详情接口获取,边播放边刷新数据。
VideoModel? detailVModel; // 短视频二级列表详情(无权限,边播边刷)
bool isLoadingDetail = false; // 详情是否加载中
String? localPath; // 下载到本地的路径
String? loadProgress; // 下载进度(百分比字符串)
String? isLoaderRunning; // 1 下载中 0 暂停
bool isSelected = false; // 列表编辑态是否勾选
String? videoTypeId; // 视频分类 id
String? videoTypeName; // 视频分类名
VideoModel({this.id});
VideoModel.fromJson(Map<dynamic, dynamic>? map) {
map ??= {};
showType = map['showType'];
richText = map['richText'];
previewStart = map['previewStart'];
// hot 后端可能下发 int / double / 字符串
final hotVal = map['hot'];
hot = hotVal is num ? hotVal.toInt() : int.tryParse('$hotVal');
if (map['permission'] is int) {
permission = map['permission'];
}
chineseLink = map['chineseLink'];
codelessLink = map["codelessLink"];
chosen = map['chosen'];
uid = map["uid"];
isTopping = map['isTopping'];
seedPlayTime = map['seedPlayTime'];
seedLinkUrl = map['seedLinkUrl'];
seedLink = map['seedLink'];
seedSize = map["seedSize"];
subid = map["subid"];
content = map["content"];
coins = map['coins'];
purchaseCount = map['purchaseCount'];
originCoins = map['originCoins'];
commentCount = map['commentCount'];
cover = map['cover'];
desc = map['desc'];
coverThumb = map['coverThumb'];
createdAt = map['createdAt'];
reviewAt = map['reviewAt'];
freeTime = map['freeTime'];
id = map['id'];
likeCount = map['likeCount'];
collectCount = map['collectCount'];
playCount = map['playCount'];
pageViewCount = map['pageViewCount'];
videoCount = map['videoCount'] ?? 0;
playTime = map['playTime'];
publisher = Publisher.fromMap(map['publisher']);
// 宽高比没返回或为 0 时兜底 16/9
final r = double.tryParse('${map['ratio'] ?? ''}') ?? 0;
ratio = r == 0 ? 16 / 9 : r;
resolution = map['resolution'];
size = map['size'];
type = map['type'];
sourceURL = map['sourceURL'];
h265Url = map['h265Url'];
previewURL = map['previewURL'];
status = map['status'];
freeArea = map['freeArea'];
showFreeTrialBadge = map['showFreeTrialBadge'];
freeTrialRemaining = map['freeTrialRemaining'];
canUseFreeTrial = map['canUseFreeTrial'];
name = map['name'];
// String 元素生成空 TagsBean,连同空名标签一起被 where 过滤掉
tags = (map['tags'] as List?)
?.map((o) => o is String ? TagsBean() : TagsBean.fromMap(o))
.where((e) => e.mergeName?.isNotEmpty == true) //过滤空字符串/空名标签
.toList();
title = map['title'];
via = map['via'];
comment =
map.containsKey("comment") ? CommentBean.fromMap(map['comment']) : null;
vidStatus = VidStatus.fromMap(map['vidStatus']);
if (map['seriesCover'] is List) {
seriesCover =
(map['seriesCover'] as List).map((e) => e.toString()).toList();
}
newsType = map['newsType'];
reason = map['reason'];
linkUrl = map['linkUrl'];
linkStr = map['linkStr'];
totalEpisode = map['totalEpisode'];
updateStatus = map['updateStatus'];
episodeNo = map['episodeNo'] ?? map['episodeNumber'];
mediaType = map['mediaType'];
vid = map['vid'];
videoType = map['videoType'];
downloadAllow = map['downloadAllow'];
videoTypeId = map['videoTypeId'];
videoTypeName = map['videoTypeName'];
lsjId = map['lsjId'];
// 刻意不解析 mediaInfovideo_detail_bottom_menu 用 `mediaInfo != null` 判定「漫画 / 长视频」,
// 一旦后端下发该字段就会把长视频误判成漫画(收藏走到 deleteBookshelf 分支)。
// 它只由客户端在 ACG 流程里显式赋值(见 video_logic 与 CartoonMediaInfo.toVideoModel),故两边都不参与序列化
}
/// 与 [VideoModel.fromJson] 严格对称:key 集合一致、嵌套对象也展开成 Map,
/// 存进本地(浏览历史 / 下载缓存记录)再读回来不丢字段。分组顺序与上方字段声明一致,便于比对。
/// 嵌套的那几个必须自己调 toJson()——只放对象时 jsonEncode 那条路能递归转,
/// 但 fromJson(toJson()) 这种内存直接往返会拿对象去喂 fromMap,抛 not a subtype
/// 两边都不参与序列化的:客户端本地态(playContentStyle / localPath / loadProgress /
/// isLoaderRunning / isSelected / detailVModel,由运行时查询回填)
/// 以及 mediaInfo(原因见 fromJson 末尾注释)。
Map<String, dynamic> toJson() => {
// 标识 / 类型
'id': id,
'vid': vid,
'subid': subid,
'lsjId': lsjId,
'uid': uid,
'newsType': newsType,
'type': type,
'videoType': videoType,
'mediaType': mediaType,
'showType': showType,
'status': status,
'reason': reason,
// 内容 / 展示
'title': title,
'name': name,
'desc': desc,
'content': content,
'richText': richText,
'linkStr': linkStr,
'cover': cover,
'coverThumb': coverThumb,
'seriesCover': seriesCover,
'tags': tags?.map((e) => e.toJson()).toList(),
'via': via,
'createdAt': createdAt,
'reviewAt': reviewAt,
'isTopping': isTopping,
'chosen': chosen,
'publisher': publisher?.toJson(),
'comment': comment?.toJson(),
// 播放源
'sourceURL': sourceURL,
'h265Url': h265Url,
'chineseLink': chineseLink,
'codelessLink': codelessLink,
'previewURL': previewURL,
'playTime': playTime,
'ratio': ratio,
'resolution': resolution,
'size': size,
'downloadAllow': downloadAllow,
// 计价 / 权限
'coins': coins,
'originCoins': originCoins,
'permission': permission,
'freeArea': freeArea,
'freeTime': freeTime,
'previewStart': previewStart,
'showFreeTrialBadge': showFreeTrialBadge,
'freeTrialRemaining': freeTrialRemaining,
'canUseFreeTrial': canUseFreeTrial,
'vidStatus': vidStatus?.toJson(),
// 统计
'hot': hot,
'likeCount': likeCount,
'collectCount': collectCount,
'commentCount': commentCount,
'playCount': playCount,
'pageViewCount': pageViewCount,
'videoCount': videoCount,
'purchaseCount': purchaseCount,
// 种子
'seedLink': seedLink,
'seedLinkUrl': seedLinkUrl,
'seedSize': seedSize,
'seedPlayTime': seedPlayTime,
// 动漫 / 漫画 / 短剧
'totalEpisode': totalEpisode,
'updateStatus': updateStatus,
//短剧缓存记录靠它显示「第N集」,不写回读盘后集数就丢了
'episodeNo': episodeNo,
// 广告
'linkUrl': linkUrl,
// 视频分类
'videoTypeId': videoTypeId,
'videoTypeName': videoTypeName,
};
// ===== 播放地址 =====
/// 拼 m3u8 完整播放链接:各播放源共用同一套 host / token / cdn 参数。
/// 后端偶尔直接下发完整链接(265、短剧下载授权地址),那种别再往前面拼一遍 host
String _m3u8(String? path) {
if (path != null &&
(path.startsWith('http://') || path.startsWith('https://')))
return path;
return "${Address.baseApiPath}/vid/h5/m3u8/$path?token=${Address.token}&c=${Address.cdnAddress}";
}
/// 源视频播放 m3u8 完整链接(H.264,兜底)
String get realVideoUrl => _m3u8(sourceURL);
/// 无码(破解版)m3u8 完整链接
String get realUncodeUrl => _m3u8(codelessLink);
/// 中文字幕版 m3u8 完整链接
String get realChineseUrl => _m3u8(chineseLink);
/// 预览片段 m3u8 完整链接
String get realPreviewUrl => _m3u8(previewURL);
/// H.265 播放 m3u8 完整链接。h265Url 已是完整 http 链接则直接用
String get realH265Url {
final u = h265Url ?? '';
if (u.isEmpty) return '';
return u.startsWith('http') ? u : _m3u8(u);
}
/// 详情页最终播放 URL(整合各分支,优先级从上到下):
/// - playContentStyle 2 且有中字链接 → 中字源
/// - playContentStyle 3 且有无码链接 → 无码源
/// - 命中预览权 [canPlayPreview] → 预览片段
/// - 普通源:[useH265] 且有 265 地址 → 265(省带宽),否则 264(兜底)
/// [useH265] 由调用方算好传入(设备硬解能力 + 本条是否已运行时回退),model 不依赖 CodecSupport
String realPlayUrl({required bool canPlayPreview, required bool useH265}) {
if (playContentStyle == 2 && chineseLink?.isNotEmpty == true)
return realChineseUrl;
if (playContentStyle == 3 && codelessLink?.isNotEmpty == true)
return realUncodeUrl;
if (canPlayPreview) return realPreviewUrl;
if (useH265 && realH265Url.isNotEmpty) return realH265Url;
return realVideoUrl;
}
// ===== 派生取值 =====
/// 展示用创建时间:createdAt 为空或后端占位值时退回审核通过时间
String? get realCreateAt {
if (createdAt?.contains("0001-01-01") == true ||
createdAt?.isNotEmpty != true) {
return reviewAt;
}
return createdAt;
}
/// 当前播放秒数是否处于免费试看区间 [previewStart, previewStart + freeTime]
bool isInFreeTime(int sec) {
final start = previewStart ?? 0;
final end = start + (freeTime ?? 0);
return sec >= start && sec < end;
}
bool get isDarkTag =>
tags?.any((e) => e.name?.contains("暗网") == true) ?? false;
String get allTags => tags?.map((e) => e.name).join(' ') ?? '';
/// 是否已点赞(vidStatus 为 null 时视为 false
bool get hasLiked => vidStatus?.hasLiked ?? false;
/// 是否已收藏(vidStatus 为 null 时视为 false
bool get hasCollected => vidStatus?.hasCollected ?? false;
/// 下载进度 0~1
double get progress => (double.tryParse(loadProgress ?? '') ?? 0) / 100;
/// 下载任务是否正在跑("2" 是已完成,不算跑)
bool get isDownloading => isLoaderRunning == "1";
/// 免费下载(downloadAllow 2):不校验 VIP / 次数,图标也换免费那套
bool get isFreeDownload => downloadAllow == 2;
String get seedSizeDesc => "${seedSize}Mb";
String get seedPlayTimeDesc {
final count = seedPlayTime ?? 0;
return count > 0 ? DateTimeUtil.formatHMS(count) : "";
}
String get seedRealLink {
if (seedLinkUrl?.isNotEmpty == true) {
var linkArr = seedLinkUrl?.split("[种子链接]:") ?? [];
if (linkArr.length == 2) {
return linkArr.last;
}
linkArr = seedLinkUrl?.split("[下载链接]:") ?? [];
if (linkArr.length == 2) {
return linkArr.last;
}
}
return seedLinkUrl ?? "";
}
bool isRandomAd() => randomAdsInfo != null;
bool isAdsArr() => adsInfoArr != null;
/// 是否需要金币购买(coins 或 originCoins 大于 0
bool isCoinVideo() => (coins ?? 0) > 0 || (originCoins ?? 0) > 0;
/// 实际售价:VIP 走折扣价,非 VIP 走原价
int? get realCoins => globalStore.isVIP ? coins : originCoins;
int videoCoin() => realCoins ?? 0;
//评论类型,传参数类型
String get commentType => 'video';
//集数状态描述
String get episodeNumberStatus => mediaInfo?.episodeNumberStatus ?? '';
/// 动漫/漫画的更新状态描述:updateStatus==1 显示"更新N集/话",其他状态一律"已完结"
String get updateDesc {
final unit = mediaType == 'image' ? '' : '';
if (updateStatus == 1) return '更新$totalEpisode$unit';
return '已完结';
}
}
/// 图集与黄游内容的解锁规则扩展
extension LockType on VideoModel {
/// 解锁样式(图集与黄游共用同一套规则):0-不需要解锁 1-金币解锁 2-会员解锁
int get lockType {
//1.免费
if (freeArea == true) return 0;
if (globalStore.isSuperUp) return 0;
//2.自己发布
if (globalStore.isMe(publisher?.uid)) return 0;
//3.金币解锁
if ((originCoins ?? 0) > 0) {
//是否购买
if (vidStatus?.hasPaid == true) return 0;
//是否免费
if (coins == 0) return 0;
return 1;
}
//vip解锁
if (globalStore.isRechargeVIP) return 0;
return 2;
}
}
/// 帖子流相关的判断与封面取值扩展(社区/个人主页等场景使用)
extension Post on VideoModel {
bool isVideo() {
return newsType == 'SP' ||
newsType == 'MOVIE' ||
newsType == 'SHORT' ||
newsType == 'COVER';
}
bool isSeedPost() {
return newsType == 'ADULT_GAME' ||
(seedLink?.isNotEmpty ?? false) ||
newsType == 'SEED_LINK';
}
}
/// 当前用户对该视频的状态:是否点赞/收藏/购买/已读等
class VidStatus {
bool? hasCollected;
bool? hasLiked;
bool? hasPaid;
int? todayPlayCnt;
int? todayRank;
bool? hasDisliked;
bool? hasPaidSeed;
bool? hasGraded;
bool? hasAddBookshelf;
bool? hasRead;
static VidStatus fromMap(Map<String, dynamic>? map) {
map ??= {};
VidStatus info = VidStatus();
info.hasCollected = map['hasCollected'];
info.hasLiked = map['hasLiked'];
info.hasPaid = map['hasPaid'];
info.todayPlayCnt = map['todayPlayCnt'];
info.todayRank = map['todayRank'];
info.hasDisliked = map['hasDisliked'];
info.hasPaidSeed = map['hasPaidSeed'];
info.hasGraded = map['hasGraded'];
info.hasAddBookshelf = map['hasAddBookshelf'];
info.hasRead = map['hasRead'];
return info;
}
Map<String, dynamic> toJson() => {
"hasDisliked": hasDisliked,
"hasCollected": hasCollected,
"hasLiked": hasLiked,
"hasPaid": hasPaid,
"todayPlayCnt": todayPlayCnt,
"todayRank": todayRank,
"hasPaidSeed": hasPaidSeed,
"hasGraded": hasGraded,
"hasAddBookshelf": hasAddBookshelf,
"hasRead": hasRead,
};
}
/// 标签 / 话题 / 分类公共模型(视频标签、帖子话题、ACG 分类共用)
class TagsBean {
String? coverImg;
String? description;
bool? hasCollected;
String? id;
String? name;
int? playCount;
int? type;
String? hotMark;
String? tagName;
int? videoCount;
int? collCount;
bool isSelected = false;
int? vidCount;
int? followCount;
/// 客户端本地态,故意不参与 fromMap / toJson:由跳转方设定,决定标签页拉哪类内容。
/// CommunityTagLogic 请求到新 tag 后会把它手动搬回来(见该 logic 的 onReady),别当僵尸字段删掉
String? newsType;
TagsBean({this.id, this.name});
//兼容name和tagname,后台返回不一致
String? get mergeName => tagName?.isNotEmpty == true ? tagName : name;
TagsBean.fromMap(Map<String, dynamic>? map) {
map ??= {};
vidCount = map['vidCount'];
followCount = map['followCount'];
hotMark = map['hotMark'];
tagName = (map['tagName'] as String?)?.trim();
videoCount = map['videoCount'];
coverImg = map['coverImg'];
description = map['description'];
hasCollected = map['hasCollected'];
id = map['id'];
name = (map['name'] as String?)?.trim();
type = map['type'];
playCount = map['playCount'];
collCount = map['collCount'];
}
Map<String, dynamic> toJson() => {
"coverImg": coverImg,
"description": description,
"hasCollected": hasCollected,
"id": id,
"name": name,
"playCount": playCount,
"tagName": tagName,
};
}
/// 视频/帖子发布者信息(用户基础资料 + VIP / 认证 / 活跃度等)
class Publisher {
int? age;
String? gender;
bool? hasFollowed;
String? name;
String? portrait;
int? uid;
int? vipLevel; //月卡 季卡 年卡
bool? isVip;
bool? superUser; //大v
int? activeValue; //活跃值
bool? officialCert; //官方认证
int? rechargeLevel; //vip等级 vip1-vip8
int? fans;
int? merchantUser; //认证商户
List<int> awards = [];
String? upTag;
int? totalWorks;
String? vipName;
Publisher();
Publisher.fromMap(Map<String, dynamic>? map) {
map ??= {};
if (map['awards'] is List) {
awards = [...(map['awards'] as List).map((o) => o)];
}
totalWorks = map['totalWorks'];
upTag = map['upTag'];
merchantUser = map['merchantUser'];
age = map['age'];
gender = map['gender'];
hasFollowed = map['hasFollowed'];
name = map['name'];
portrait = map['portrait'];
uid = map['uid'];
vipLevel = map['vipLevel'];
isVip = map['isVip'];
if (map['superUser'] is bool) {
superUser = map['superUser'];
} else if (map['superUser'] is int) {
superUser = map['superUser'] >= 1;
}
activeValue = map['activeValue'];
officialCert = map['officialCert'];
rechargeLevel = map['rechargeLevel'];
fans = map['fans'];
vipName = map['vipName'];
}
Map<String, dynamic> toJson() => {
"age": age,
"gender": gender,
"hasFollowed": hasFollowed,
"name": name,
"portrait": portrait,
"uid": uid,
"vipLevel": vipLevel,
"isVip": isVip,
"superUser": superUser,
"activeValue": activeValue,
"officialCert": officialCert,
"rechargeLevel": rechargeLevel,
};
}
/// 详情页热评(取一条最热评论附带在视频/帖子主体里展示)
class CommentBean {
int? uid;
String? name;
String? portrait;
String? cid;
String? content;
int? likeCount;
bool? isAuthor;
String? createdAt;
CommentBean.fromMap(Map<String, dynamic>? map) {
map ??= {};
uid = map['uid'];
name = map['name'];
portrait = map['portrait'];
cid = map['cid'];
content = map['content'];
likeCount = map['likeCount'];
isAuthor = map['isAuthor'];
createdAt = map['createdAt'];
}
Map<String, dynamic> toJson() => {
"uid": uid,
"name": name,
"portrait": portrait,
"cid": cid,
"content": content,
"likeCount": likeCount,
"isAuthor": isAuthor,
"createdAt": createdAt,
};
}