初始化
This commit is contained in:
@@ -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'];
|
||||
// 刻意不解析 mediaInfo:video_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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user