670 lines
22 KiB
Dart
670 lines
22 KiB
Dart
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,
|
||
};
|
||
}
|