187 lines
5.9 KiB
Dart
187 lines
5.9 KiB
Dart
// 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,
|
||
this.thumbnailsUrl,
|
||
});
|
||
|
||
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;
|
||
final String? thumbnailsUrl;
|
||
|
||
Episode withVideoUrl(
|
||
String url, {
|
||
int? likeCount,
|
||
int? commentCount,
|
||
String? thumbnailsUrl,
|
||
}) => Episode(
|
||
id: id,
|
||
number: number,
|
||
title: title,
|
||
isFree: isFree,
|
||
videoUrl: url,
|
||
durationSec: durationSec,
|
||
locked: locked,
|
||
likeCount: likeCount ?? this.likeCount,
|
||
commentCount: commentCount ?? this.commentCount,
|
||
thumbnailsUrl: thumbnailsUrl ?? this.thumbnailsUrl,
|
||
);
|
||
|
||
factory Episode.fromJson(Map<String, dynamic> 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']),
|
||
thumbnailsUrl: _optionalString(json['thumbnailsUrl']),
|
||
);
|
||
}
|
||
}
|
||
|
||
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.dramaType = '',
|
||
this.createdAt,
|
||
this.detailLoaded = false,
|
||
});
|
||
|
||
final String id;
|
||
final String title;
|
||
final String synopsis;
|
||
final String posterUrl;
|
||
final List<Episode> 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;
|
||
|
||
/// 内容类型:ANIME 漫剧,LIVE 真人剧。
|
||
final String dramaType;
|
||
|
||
/// 上架时间;卡片接口未直接下发时可由 Snowflake id 估算。
|
||
final DateTime? createdAt;
|
||
|
||
/// 详情接口是否已经取过完整剧集列表。
|
||
/// 列表卡片只带一集可播预览,需要完整剧集时再进详情。
|
||
final bool detailLoaded;
|
||
|
||
/// 卡片只带一集预览,集数以后端给的总集数为准。
|
||
int get episodeCount => totalEpisodes > 0 ? totalEpisodes : episodes.length;
|
||
|
||
/// 列表卡片:带一集可播预览(首页直接起播),完整剧集进详情再取。
|
||
factory Drama.fromCardJson(Map<String, dynamic> 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<String, dynamic>
|
||
? [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? ?? '',
|
||
dramaType: json['dramaType'] as String? ?? '',
|
||
createdAt: DateTime.tryParse(json['createdAt']?.toString() ?? ''),
|
||
);
|
||
}
|
||
|
||
/// 详情:带剧集列表。
|
||
factory Drama.fromDetailJson(Map<String, dynamic> 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? ?? '',
|
||
dramaType: json['dramaType'] as String? ?? '',
|
||
createdAt: DateTime.tryParse(json['createdAt']?.toString() ?? ''),
|
||
detailLoaded: true,
|
||
episodes: (json['episodes'] as List? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Episode.fromJson)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
int? _optionalInt(dynamic value) => value == null ? null : jsonToInt(value);
|
||
|
||
String? _optionalString(dynamic value) {
|
||
final text = value is String ? value.trim() : '';
|
||
return text.isEmpty ? null : text;
|
||
}
|