// Author: fruit.com.cn import 'json_numbers.dart'; /// 短剧与剧集,字段与后端 `/app/**` 的下发结构对应。 /// /// 封面与播放地址都是后端返回的相对路径(`/files/...`), /// 渲染前用 `AppConfig.mediaUrl` 补上服务地址。 class Episode { const Episode({ required this.id, required this.number, required this.title, required this.isFree, this.videoUrl = '', this.durationSec = 0, this.locked = false, this.likeCount, this.commentCount, }); final String id; final int number; final String title; final bool isFree; /// 播放地址:详情接口不下发,由 `/app/episodes/{id}/play` 返回后写入。 final String videoUrl; final int durationSec; /// 后端判定需要解锁;解锁能力(金币/VIP)在 M3 接入。 final bool locked; final int? likeCount; final int? commentCount; Episode withVideoUrl(String url, {int? likeCount, int? commentCount}) => Episode( id: id, number: number, title: title, isFree: isFree, videoUrl: url, durationSec: durationSec, locked: locked, likeCount: likeCount ?? this.likeCount, commentCount: commentCount ?? this.commentCount, ); factory Episode.fromJson(Map json) { return Episode( id: (json['id'] ?? '').toString(), number: (json['number'] as num?)?.toInt() ?? 0, title: json['title'] as String? ?? '', isFree: json['isFree'] == true, durationSec: (json['durationSec'] as num?)?.toInt() ?? 0, locked: json['locked'] == true, videoUrl: json['playUrl'] as String? ?? '', likeCount: _optionalInt(json['likeCount']), commentCount: _optionalInt(json['commentCount']), ); } } class Drama { const Drama({ required this.id, required this.title, required this.synopsis, required this.posterUrl, this.episodes = const [], this.category = '', this.genre = '', this.totalEpisodes = 0, this.freeEpisodes = 0, this.isHot = false, this.viewCount = 0, this.likeCount, this.favoriteCount, this.commentCount, this.language = '', this.detailLoaded = false, }); final String id; final String title; final String synopsis; final String posterUrl; final List episodes; final String category; final String genre; final int totalEpisodes; final int freeEpisodes; final bool isHot; final int viewCount; /// 详情下发的互动数量;卡片或旧接口没有这些字段时为 null(客户端不显示数字)。 final int? likeCount; final int? favoriteCount; final int? commentCount; /// 卡片语种(如 zh-CN / id),用于剧场语言筛选。 final String language; /// 详情接口是否已经取过完整剧集列表。 /// 列表卡片只带一集可播预览,需要完整剧集时再进详情。 final bool detailLoaded; /// 卡片只带一集预览,集数以后端给的总集数为准。 int get episodeCount => totalEpisodes > 0 ? totalEpisodes : episodes.length; /// 列表卡片:带一集可播预览(首页直接起播),完整剧集进详情再取。 factory Drama.fromCardJson(Map json) { final preview = json['preview']; return Drama( id: (json['id'] ?? '').toString(), title: json['title'] as String? ?? '', synopsis: json['synopsis'] as String? ?? '', posterUrl: json['cover'] as String? ?? '', episodes: preview is Map ? [Episode.fromJson(preview)] : const [], category: json['category'] as String? ?? '', genre: json['genre'] as String? ?? '', totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0, isHot: json['isHot'] == true, // viewCount 后端是 Long,按字符串下发(见 json_numbers.dart) viewCount: jsonToInt(json['viewCount']), likeCount: _optionalInt(json['likeCount']), favoriteCount: _optionalInt(json['favoriteCount']), commentCount: _optionalInt(json['commentCount']), language: json['language'] as String? ?? '', ); } /// 详情:带剧集列表。 factory Drama.fromDetailJson(Map json) { return Drama( id: (json['id'] ?? '').toString(), title: json['title'] as String? ?? '', synopsis: json['synopsis'] as String? ?? '', posterUrl: json['cover'] as String? ?? '', category: json['category'] as String? ?? '', genre: json['genre'] as String? ?? '', totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0, freeEpisodes: (json['freeEpisodes'] as num?)?.toInt() ?? 0, isHot: json['isHot'] == true, viewCount: jsonToInt(json['viewCount']), likeCount: _optionalInt(json['likeCount']), favoriteCount: _optionalInt(json['favoriteCount']), commentCount: _optionalInt(json['commentCount']), language: json['language'] as String? ?? '', detailLoaded: true, episodes: (json['episodes'] as List? ?? const []) .whereType>() .map(Episode.fromJson) .toList(), ); } } int? _optionalInt(dynamic value) => value == null ? null : jsonToInt(value);