part of "history.dart"; class ImageFavorite { final String eid; final String id; // 漫画id final int ep; final String epName; final String sourceKey; String imageKey; int page; bool? isAutoFavorite; ImageFavorite( this.page, this.imageKey, this.isAutoFavorite, this.eid, this.id, this.ep, this.sourceKey, this.epName, ); Map toJson() { return { 'page': page, 'imageKey': imageKey, 'isAutoFavorite': isAutoFavorite, 'eid': eid, 'id': id, 'ep': ep, 'sourceKey': sourceKey, 'epName': epName, }; } ImageFavorite.fromJson(Map json) : page = json['page'], imageKey = json['imageKey'], isAutoFavorite = json['isAutoFavorite'], eid = json['eid'], id = json['id'], ep = json['ep'], sourceKey = json['sourceKey'], epName = json['epName']; ImageFavorite copyWith({ int? page, String? imageKey, bool? isAutoFavorite, String? eid, String? id, int? ep, String? sourceKey, String? epName, }) { return ImageFavorite( page ?? this.page, imageKey ?? this.imageKey, isAutoFavorite ?? this.isAutoFavorite, eid ?? this.eid, id ?? this.id, ep ?? this.ep, sourceKey ?? this.sourceKey, epName ?? this.epName, ); } @override bool operator ==(Object other) { return other is ImageFavorite && other.id == id && other.sourceKey == sourceKey && other.page == page && other.eid == eid && other.ep == ep; } @override int get hashCode => Object.hash(id, sourceKey, page, eid, ep); } class ImageFavoritesEp { // 小心拷贝等多章节的可能更新章节顺序 String eid; final int ep; int maxPage; String epName; List imageFavorites; ImageFavoritesEp( this.eid, this.ep, this.imageFavorites, this.epName, this.maxPage); // 是否有封面 bool get isHasFirstPage { return imageFavorites[0].page == firstPage; } // 是否都有imageKey bool get isHasImageKey { return imageFavorites.every((e) => e.imageKey != ""); } Map toJson() { return { 'eid': eid, 'ep': ep, 'maxPage': maxPage, 'epName': epName, 'imageFavorites': imageFavorites.map((e) => e.toJson()).toList(), }; } } class ImageFavoritesComic { final String id; final String title; String subTitle; String author; final String sourceKey; // 不一定是真的这本漫画的所有页数, 如果是多章节的时候 int maxPage; List tags; List translatedTags; final DateTime time; List imageFavoritesEp; final Map other; ImageFavoritesComic( this.id, this.imageFavoritesEp, this.title, this.sourceKey, this.tags, this.translatedTags, this.time, this.author, this.other, this.subTitle, this.maxPage, ); // 是否都有imageKey bool get isAllHasImageKey { return imageFavoritesEp .every((e) => e.imageFavorites.every((j) => j.imageKey != "")); } int get maxPageFromEp { int temp = 0; for (var e in imageFavoritesEp) { temp += e.maxPage; } return temp; } // 是否都有封面 bool get isAllHasFirstPage { return imageFavoritesEp.every((e) => e.isHasFirstPage); } Iterable get images sync*{ for (var e in imageFavoritesEp) { yield* e.imageFavorites; } } @override bool operator ==(Object other) { return other is ImageFavoritesComic && other.id == id && other.sourceKey == sourceKey; } @override int get hashCode => Object.hash(id, sourceKey); factory ImageFavoritesComic.fromRow(Row r) { var tempImageFavoritesEp = jsonDecode(r["image_favorites_ep"]); List finalImageFavoritesEp = []; tempImageFavoritesEp.forEach((i) { List temp = []; i["imageFavorites"].forEach((j) { temp.add(ImageFavorite( j["page"], j["imageKey"], j["isAutoFavorite"], i["eid"], r["id"], i["ep"], r["source_key"], i["epName"], )); }); finalImageFavoritesEp.add(ImageFavoritesEp( i["eid"], i["ep"], temp, i["epName"], i["maxPage"] ?? 1)); }); return ImageFavoritesComic( r["id"], finalImageFavoritesEp, r["title"], r["source_key"], r["tags"].split(","), r["translated_tags"].split(","), DateTime.fromMillisecondsSinceEpoch(r["time"]), r["author"], jsonDecode(r["other"]), r["sub_title"], r["max_page"], ); } } class ImageFavoriteManager with ChangeNotifier { Database get _db => HistoryManager()._db; List get comics => getAll(); static ImageFavoriteManager? _cache; ImageFavoriteManager._(); factory ImageFavoriteManager() => (_cache ??= ImageFavoriteManager._()); /// 检查表image_favorites是否存在, 不存在则创建 void init() { _db.execute("CREATE TABLE IF NOT EXISTS image_favorites (" "id TEXT," "title TEXT NOT NULL," "sub_title TEXT," "author TEXT," "tags TEXT," "translated_tags TEXT," "time int," "max_page int," "source_key TEXT NOT NULL," "image_favorites_ep TEXT NOT NULL," "other TEXT NOT NULL," "PRIMARY KEY (id,source_key)" ");"); } // 做排序和去重的操作 void addOrUpdateOrDelete(ImageFavoritesComic favorite, [bool notify = true]) { // 没有章节了就删掉 if (favorite.imageFavoritesEp.isEmpty) { _db.execute(""" delete from image_favorites where id == ? and source_key == ?; """, [favorite.id, favorite.sourceKey]); } else { // 去重章节 List tempImageFavoritesEp = []; for (var e in favorite.imageFavoritesEp) { int index = tempImageFavoritesEp.indexWhere((i) { return i.ep == e.ep; }); // 再做一层保险, 防止出现ep为0的脏数据 if (index == -1 && e.ep > 0) { tempImageFavoritesEp.add(e); } } tempImageFavoritesEp.sort((a, b) => a.ep.compareTo(b.ep)); List finalImageFavoritesEp = jsonDecode(jsonEncode(tempImageFavoritesEp)); for (var e in tempImageFavoritesEp) { List finalImageFavorites = []; int epIndex = tempImageFavoritesEp.indexOf(e); for (ImageFavorite j in e.imageFavorites) { int index = finalImageFavorites.indexWhere((i) => i["page"] == j.page); if (index == -1 && j.page > 0) { // isAutoFavorite 为 null 不写入数据库, 同时只保留需要的属性, 避免增加太多重复字段在数据库里 if (j.isAutoFavorite != null) { finalImageFavorites.add({ "page": j.page, "imageKey": j.imageKey, "isAutoFavorite": j.isAutoFavorite }); } else { finalImageFavorites.add({"page": j.page, "imageKey": j.imageKey}); } } } finalImageFavorites.sort((a, b) => a["page"].compareTo(b["page"])); finalImageFavoritesEp[epIndex]["imageFavorites"] = finalImageFavorites; } if (tempImageFavoritesEp.isEmpty) { throw "Error: No ImageFavoritesEp"; } _db.execute(""" insert or replace into image_favorites(id, title, sub_title, author, tags, translated_tags, time, max_page, source_key, image_favorites_ep, other) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, [ favorite.id, favorite.title, favorite.subTitle, favorite.author, favorite.tags.join(","), favorite.translatedTags.join(","), favorite.time.millisecondsSinceEpoch, favorite.maxPage, favorite.sourceKey, jsonEncode(finalImageFavoritesEp), jsonEncode(favorite.other) ]); } if (notify) { notifyListeners(); } } bool has(String id, String sourceKey, String eid, int page, int ep) { var comic = find(id, sourceKey); if (comic == null) { return false; } var epIndex = comic.imageFavoritesEp.where((e) => e.eid == eid).firstOrNull; if (epIndex == null) { return false; } return epIndex.imageFavorites.any((e) => e.page == page && e.ep == ep); } List getAll([String? keyword]) { ResultSet res; if (keyword == null || keyword == "") { res = _db.select("select * from image_favorites;"); } else { res = _db.select( """ select * from image_favorites WHERE title LIKE ? OR sub_title LIKE ? OR LOWER(tags) LIKE LOWER(?) OR LOWER(translated_tags) LIKE LOWER(?) OR author LIKE ?; """, ['%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%'], ); } try { return res.map((e) => ImageFavoritesComic.fromRow(e)).toList(); } catch (e, stackTrace) { Log.error("Unhandled Exception", e.toString(), stackTrace); return []; } } void deleteImageFavorite(Iterable imageFavoriteList) { if (imageFavoriteList.isEmpty) { return; } for (var i in imageFavoriteList) { ImageFavoritesProvider.deleteFromCache(i); } var comics = {}; for (var i in imageFavoriteList) { var comic = comics .where((c) => c.id == i.id && c.sourceKey == i.sourceKey) .firstOrNull ?? find(i.id, i.sourceKey); if (comic == null) { continue; } var ep = comic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == i.ep); if (ep == null) { continue; } ep.imageFavorites.remove(i); if (ep.imageFavorites.isEmpty) { comic.imageFavoritesEp.remove(ep); } comics.add(comic); } for (var i in comics) { addOrUpdateOrDelete(i, false); } notifyListeners(); } int get length { var res = _db.select("select count(*) from image_favorites;"); return res.first.values.first! as int; } List search(String keyword) { if (keyword == "") { return []; } return getAll(keyword); } static Future computeImageFavorites() { var token = ServicesBinding.rootIsolateToken!; var count = ImageFavoriteManager().length; if (count == 0) { return Future.value(ImageFavoritesComputed([], [], [])); } else if (count > 100) { return Isolate.run(() async { BackgroundIsolateBinaryMessenger.ensureInitialized(token); await App.init(); await HistoryManager().init(); return _computeImageFavorites(); }); } else { return Future.value(_computeImageFavorites()); } } static ImageFavoritesComputed _computeImageFavorites() { const maxLength = 20; var comics = ImageFavoriteManager().getAll(); // 去掉这些没有意义的标签 const List exceptTags = [ '連載中', '', 'translated', 'chinese', 'sole male', 'sole female', 'original', 'doujinshi', 'manga', 'multi-work series', 'mosaic censorship', 'dilf', 'bbm', 'uncensored', 'full censorship' ]; Map tagCount = {}; Map authorCount = {}; Map comicImageCount = {}; Map comicMaxPages = {}; for (var comic in comics) { for (var tag in comic.tags) { String finalTag = tag; tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1; } if (comic.author != "") { String finalAuthor = comic.author; authorCount[finalAuthor] = (authorCount[finalAuthor] ?? 0) + comic.images.length; } // 小于10页的漫画不统计 if (comic.maxPageFromEp < 10) { continue; } comicImageCount[comic] = (comicImageCount[comic] ?? 0) + comic.images.length; comicMaxPages[comic] = (comicMaxPages[comic] ?? 0) + comic.maxPageFromEp; } // 按数量排序标签 List sortedTags = tagCount.keys.toList() ..sort((a, b) => tagCount[b]!.compareTo(tagCount[a]!)); // 按数量排序作者 List sortedAuthors = authorCount.keys.toList() ..sort((a, b) => authorCount[b]!.compareTo(authorCount[a]!)); // 按收藏数量排序漫画 List> sortedComicsByNum = comicImageCount.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); validateTag(String tag) { if (tag.startsWith("Category:")) { return false; } return !exceptTags.contains(tag.split(":").last.toLowerCase()) && !tag.isNum; } return ImageFavoritesComputed( sortedTags .where(validateTag) .map((tag) => TextWithCount(tag, tagCount[tag]!)) .take(maxLength) .toList(), sortedAuthors .map((author) => TextWithCount(author, authorCount[author]!)) .take(maxLength) .toList(), sortedComicsByNum .map((comic) => TextWithCount(comic.key.title, comic.value)) .take(maxLength) .toList(), ); } ImageFavoritesComic? find(String id, String sourceKey) { var row = _db.select(""" select * from image_favorites where id == ? and source_key == ?; """, [id, sourceKey]); if (row.isEmpty) { return null; } return ImageFavoritesComic.fromRow(row.first); } } class TextWithCount { final String text; final int count; const TextWithCount(this.text, this.count); } class ImageFavoritesComputed { /// 基于收藏的标签数排序 final List tags; /// 基于收藏的作者数排序 final List authors; /// 基于喜欢的图片数排序 final List comics; /// 计算后的图片收藏数据 const ImageFavoritesComputed( this.tags, this.authors, this.comics, ); bool get isEmpty => tags.isEmpty && authors.isEmpty && comics.isEmpty; }