Files
venera/lib/foundation/comic_source/models.dart

427 lines
10 KiB
Dart

part of 'comic_source.dart';
class Comment {
final String userName;
final String? avatar;
final String content;
final String? time;
final int? replyCount;
final String? id;
int? score;
final bool? isLiked;
int? voteStatus; // 1: upvote, -1: downvote, 0: none
static String? parseTime(dynamic value) {
if (value == null) return null;
if (value is int) {
if (value < 10000000000) {
return DateTime.fromMillisecondsSinceEpoch(value * 1000)
.toString()
.substring(0, 19);
} else {
return DateTime.fromMillisecondsSinceEpoch(value)
.toString()
.substring(0, 19);
}
}
return value.toString();
}
Comment.fromJson(Map<String, dynamic> json)
: userName = json["userName"],
avatar = json["avatar"],
content = json["content"],
time = parseTime(json["time"]),
replyCount = json["replyCount"],
id = json["id"].toString(),
score = json["score"],
isLiked = json["isLiked"],
voteStatus = json["voteStatus"];
}
class Comic {
final String title;
final String cover;
final String id;
final String? subtitle;
final List<String>? tags;
final String description;
final String sourceKey;
final int? maxPage;
final String? language;
final String? favoriteId;
/// 0-5
final double? stars;
const Comic(
this.title,
this.cover,
this.id,
this.subtitle,
this.tags,
this.description,
this.sourceKey,
this.maxPage,
this.language,
) : favoriteId = null,
stars = null;
Map<String, dynamic> toJson() {
return {
"title": title,
"cover": cover,
"id": id,
"subTitle": subtitle,
"tags": tags,
"description": description,
"sourceKey": sourceKey,
"maxPage": maxPage,
"language": language,
"favoriteId": favoriteId,
};
}
Comic.fromJson(Map<String, dynamic> json, this.sourceKey)
: title = json["title"],
subtitle = json["subtitle"] ?? json["subTitle"] ?? "",
cover = json["cover"],
id = json["id"],
tags = List<String>.from(json["tags"] ?? []),
description = json["description"] ?? "",
maxPage = json["maxPage"],
language = json["language"],
favoriteId = json["favoriteId"],
stars = (json["stars"] as num?)?.toDouble();
@override
bool operator ==(Object other) {
if (other is! Comic) return false;
return other.id == id && other.sourceKey == sourceKey;
}
@override
int get hashCode => id.hashCode ^ sourceKey.hashCode;
}
class ComicDetails with HistoryMixin {
@override
final String title;
@override
final String? subTitle;
@override
final String cover;
final String? description;
final Map<String, List<String>> tags;
/// id-name
final ComicChapters? chapters;
final List<String>? thumbnails;
final List<Comic>? recommend;
final String sourceKey;
final String comicId;
final bool? isFavorite;
final String? subId;
final bool? isLiked;
final int? likesCount;
final int? commentCount;
final String? uploader;
final String? uploadTime;
final String? updateTime;
final String? url;
final double? stars;
@override
final int? maxPage;
final List<Comment>? comments;
static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) {
var res = <String, List<String>>{};
map.forEach((key, value) {
res[key] = List<String>.from(value);
});
return res;
}
ComicDetails.fromJson(Map<String, dynamic> json)
: title = json["title"],
subTitle = json["subtitle"],
cover = json["cover"],
description = json["description"],
tags = _generateMap(json["tags"]),
chapters = ComicChapters.fromJsonOrNull(json["chapters"]),
sourceKey = json["sourceKey"],
comicId = json["comicId"],
thumbnails = ListOrNull.from(json["thumbnails"]),
recommend = (json["recommend"] as List?)
?.map((e) => Comic.fromJson(e, json["sourceKey"]))
.toList(),
isFavorite = json["isFavorite"],
subId = json["subId"],
likesCount = json["likesCount"],
isLiked = json["isLiked"],
commentCount = json["commentCount"],
uploader = json["uploader"],
uploadTime = json["uploadTime"],
updateTime = json["updateTime"],
url = json["url"],
stars = (json["stars"] as num?)?.toDouble(),
maxPage = json["maxPage"],
comments = (json["comments"] as List?)
?.map((e) => Comment.fromJson(e))
.toList();
Map<String, dynamic> toJson() {
return {
"title": title,
"subTitle": subTitle,
"cover": cover,
"description": description,
"tags": tags,
"chapters": chapters,
"thumbnails": thumbnails,
"recommend": null,
"sourceKey": sourceKey,
"comicId": comicId,
"isFavorite": isFavorite,
"subId": subId,
"isLiked": isLiked,
"likesCount": likesCount,
"commentsCount": commentCount,
"uploader": uploader,
"uploadTime": uploadTime,
"updateTime": updateTime,
"url": url,
};
}
@override
HistoryType get historyType => HistoryType(sourceKey.hashCode);
@override
String get id => comicId;
ComicType get comicType => ComicType(sourceKey.hashCode);
/// Convert tags map to plain list
List<String> get plainTags {
var res = <String>[];
tags.forEach((key, value) {
res.addAll(value.map((e) => "$key:$e"));
});
return res;
}
/// Find the first author tag
String? findAuthor() {
var authorNamespaces = [
"author",
"authors",
"artist",
"artists",
"作者",
"画师"
];
for (var entry in tags.entries) {
if (authorNamespaces.contains(entry.key.toLowerCase()) &&
entry.value.isNotEmpty) {
return entry.value.first;
}
}
return null;
}
String? _validateUpdateTime(String time) {
time = time.split(" ").first;
var segments = time.split("-");
if (segments.length != 3) return null;
var year = int.tryParse(segments[0]);
var month = int.tryParse(segments[1]);
var day = int.tryParse(segments[2]);
if (year == null || month == null || day == null) return null;
if (year < 2000 || year > 3000) return null;
if (month < 1 || month > 12) return null;
if (day < 1 || day > 31) return null;
return "$year-$month-$day";
}
String? findUpdateTime() {
if (updateTime != null) {
return _validateUpdateTime(updateTime!);
}
const acceptedNamespaces = [
"更新",
"最後更新",
"最后更新",
"update",
"last update",
];
for (var entry in tags.entries) {
if (acceptedNamespaces.contains(entry.key.toLowerCase()) &&
entry.value.isNotEmpty) {
var value = entry.value.first;
return _validateUpdateTime(value);
}
}
return null;
}
}
class ArchiveInfo {
final String title;
final String description;
final String id;
ArchiveInfo.fromJson(Map<String, dynamic> json)
: title = json["title"],
description = json["description"],
id = json["id"];
}
class ComicChapters {
final Map<String, String>? _chapters;
final Map<String, Map<String, String>>? _groupedChapters;
/// Create a ComicChapters object with a flat map
const ComicChapters(Map<String, String> this._chapters)
: _groupedChapters = null;
/// Create a ComicChapters object with a grouped map
const ComicChapters.grouped(
Map<String, Map<String, String>> this._groupedChapters)
: _chapters = null;
factory ComicChapters.fromJson(dynamic json) {
if (json is! Map) throw ArgumentError("Invalid json type");
var chapters = <String, String>{};
var groupedChapters = <String, Map<String, String>>{};
for (var entry in json.entries) {
var key = entry.key;
var value = entry.value;
if (key is! String) throw ArgumentError("Invalid key type");
if (value is Map) {
groupedChapters[key] = Map.from(value);
} else {
chapters[key] = value.toString();
}
}
if (chapters.isNotEmpty) {
return ComicChapters(chapters);
} else {
return ComicChapters.grouped(groupedChapters);
}
}
static fromJsonOrNull(dynamic json) {
if (json == null) return null;
return ComicChapters.fromJson(json);
}
Map<String, dynamic> toJson() {
if (_chapters != null) {
return _chapters;
} else {
return _groupedChapters!;
}
}
/// Whether the chapters are grouped
bool get isGrouped => _groupedChapters != null;
/// All group names
Iterable<String> get groups => _groupedChapters?.keys ?? [];
/// All chapters.
/// If the chapters are grouped, all groups will be merged.
Map<String, String> get allChapters {
if (_chapters != null) return _chapters;
var res = <String, String>{};
for (var entry in _groupedChapters!.values) {
res.addAll(entry);
}
return res;
}
/// Get a group of chapters by name
Map<String, String> getGroup(String group) {
return _groupedChapters![group] ?? {};
}
/// Get a group of chapters by index(0-based)
Map<String, String> getGroupByIndex(int index) {
return _groupedChapters!.values.elementAt(index);
}
/// Get total number of chapters
int get length {
return isGrouped
? _groupedChapters!.values.map((e) => e.length).reduce((a, b) => a + b)
: _chapters!.length;
}
/// Get the number of groups
int get groupCount => _groupedChapters?.length ?? 0;
/// Iterate all chapter ids
Iterable<String> get ids sync* {
if (isGrouped) {
for (var entry in _groupedChapters!.values) {
yield* entry.keys;
}
} else {
yield* _chapters!.keys;
}
}
/// Iterate all chapter titles
Iterable<String> get titles sync* {
if (isGrouped) {
for (var entry in _groupedChapters!.values) {
yield* entry.values;
}
} else {
yield* _chapters!.values;
}
}
String? operator [](String key) {
if (isGrouped) {
for (var entry in _groupedChapters!.values) {
if (entry.containsKey(key)) return entry[key];
}
return null;
} else {
return _chapters![key];
}
}
}