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 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? 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 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 json, this.sourceKey) : title = json["title"], subtitle = json["subtitle"] ?? json["subTitle"] ?? "", cover = json["cover"], id = json["id"], tags = List.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; @override toString() => "$sourceKey@$id"; } class ComicDetails with HistoryMixin { @override final String title; @override final String? subTitle; @override final String cover; final String? description; final Map> tags; /// id-name final ComicChapters? chapters; final List? thumbnails; final List? 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? comments; static Map> _generateMap(Map map) { var res = >{}; map.forEach((key, value) { res[key] = List.from(value); }); return res; } ComicDetails.fromJson(Map 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 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 get plainTags { var res = []; 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 json) : title = json["title"], description = json["description"], id = json["id"]; } class ComicChapters { final Map? _chapters; final Map>? _groupedChapters; /// Create a ComicChapters object with a flat map const ComicChapters(Map this._chapters) : _groupedChapters = null; /// Create a ComicChapters object with a grouped map const ComicChapters.grouped( Map> this._groupedChapters) : _chapters = null; factory ComicChapters.fromJson(dynamic json) { if (json is! Map) throw ArgumentError("Invalid json type"); var chapters = {}; var groupedChapters = >{}; 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 if (groupedChapters.isNotEmpty) { return ComicChapters.grouped(groupedChapters); } else { // return a empty list. return ComicChapters(chapters); } } static fromJsonOrNull(dynamic json) { if (json == null) return null; return ComicChapters.fromJson(json); } Map toJson() { if (_chapters != null) { return _chapters; } else { return _groupedChapters!; } } /// Whether the chapters are grouped bool get isGrouped => _groupedChapters != null; /// All group names Iterable get groups => _groupedChapters?.keys ?? []; /// All chapters. /// If the chapters are grouped, all groups will be merged. Map get allChapters { if (_chapters != null) return _chapters; var res = {}; for (var entry in _groupedChapters!.values) { res.addAll(entry); } return res; } /// Get a group of chapters by name Map getGroup(String group) { return _groupedChapters![group] ?? {}; } /// Get a group of chapters by index(0-based) Map 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 get ids sync* { if (isGrouped) { for (var entry in _groupedChapters!.values) { yield* entry.keys; } } else { yield* _chapters!.keys; } } /// Iterate all chapter titles Iterable 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]; } } } class PageJumpTarget { final String sourceKey; final String page; final Map? attributes; const PageJumpTarget(this.sourceKey, this.page, this.attributes); static PageJumpTarget parse(String sourceKey, dynamic value) { if (value is Map) { if (value['page'] != null) { return PageJumpTarget( sourceKey, value["page"] ?? "search", value["attributes"], ); } else if (value["action"] != null) { // old version `onClickTag` var page = value["action"]; if (page == "search") { return PageJumpTarget( sourceKey, "search", { "text": value["keyword"], }, ); } else if (page == "category") { return PageJumpTarget( sourceKey, "category", { "category": value["keyword"], "param": value["param"], }, ); } else { return PageJumpTarget(sourceKey, page, null); } } } else if (value is String) { // old version string encoding. search: `search:keyword`, category: `category:keyword` or `category:keyword@param` var segments = value.split(":"); var page = segments[0]; if (page == "search") { return PageJumpTarget( sourceKey, "search", { "text": segments[1], }, ); } else if (page == "category") { var c = segments[1]; if (c.contains('@')) { var parts = c.split('@'); return PageJumpTarget( sourceKey, "category", { "category": parts[0], "param": parts[1], }, ); } else { return PageJumpTarget( sourceKey, "category", { "category": c, }, ); } } else { return PageJumpTarget(sourceKey, page, null); } } return PageJumpTarget(sourceKey, "Invalid Data", null); } void jump(BuildContext context) { if (page == "search") { context.to( () => SearchResultPage( text: attributes?["text"] ?? attributes?["keyword"] ?? "", sourceKey: sourceKey, options: List.from(attributes?["options"] ?? []), ), ); } else if (page == "category") { var key = ComicSource.find(sourceKey)!.categoryData!.key; context.to( () => CategoryComicsPage( categoryKey: key, category: attributes?["category"] ?? (throw ArgumentError("Category name is required")), options: List.from(attributes?["options"] ?? []), param: attributes?["param"], ), ); } else { Log.error("Page Jump", "Unknown page: $page"); } } }