part of 'comic_source.dart'; bool compareSemVer(String ver1, String ver2) { ver1 = ver1.replaceFirst("-", "."); ver2 = ver2.replaceFirst("-", "."); List v1 = ver1.split('.'); List v2 = ver2.split('.'); for (int i = 0; i < 3; i++) { int num1 = int.parse(v1[i]); int num2 = int.parse(v2[i]); if (num1 > num2) { return true; } else if (num1 < num2) { return false; } } var v14 = v1.elementAtOrNull(3); var v24 = v2.elementAtOrNull(3); if (v14 != v24) { if (v14 == null && v24 != "hotfix") { return true; } else if (v14 == null) { return false; } if (v24 == null) { if (v14 == "hotfix") { return true; } return false; } return v14.compareTo(v24) > 0; } return false; } class ComicSourceParseException implements Exception { final String message; ComicSourceParseException(this.message); @override String toString() { return message; } } class ComicSourceParser { /// comic source key String? _key; String? _name; Future createAndParse(String js, String fileName) async { if (!fileName.endsWith("js")) { fileName = "$fileName.js"; } var file = File(FilePath.join(App.dataPath, "comic_source", fileName)); if (file.existsSync()) { int i = 0; while (file.existsSync()) { file = File(FilePath.join(App.dataPath, "comic_source", "${fileName.split('.').first}($i).js")); i++; } } await file.writeAsString(js); try { return await parse(js, file.path); } catch (e) { await file.delete(); rethrow; } } Future parse(String js, String filePath) async { js = js.replaceAll("\r\n", "\n"); var line1 = js .split('\n') .firstWhereOrNull((element) => element.removeAllBlank.isNotEmpty); if (line1 == null || !line1.startsWith("class ") || !line1.contains("extends ComicSource")) { throw ComicSourceParseException("Invalid Content"); } var className = line1.split("class")[1].split("extends ComicSource").first; className = className.trim(); JsEngine().runCode(""" (() => { $js this['temp'] = new $className() }).call() """); _name = JsEngine().runCode("this['temp'].name") ?? (throw ComicSourceParseException('name is required')); var key = JsEngine().runCode("this['temp'].key") ?? (throw ComicSourceParseException('key is required')); var version = JsEngine().runCode("this['temp'].version") ?? (throw ComicSourceParseException('version is required')); var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion"); var url = JsEngine().runCode("this['temp'].url"); if (minAppVersion != null) { if (compareSemVer(minAppVersion, App.version.split('-').first)) { throw ComicSourceParseException( "minAppVersion $minAppVersion is required"); } } for (var source in ComicSource.all()) { if (source.key == key) { throw ComicSourceParseException("key($key) already exists"); } } _key = key; _checkKeyValidation(); JsEngine().runCode(""" ComicSource.sources.$_key = this['temp']; """); var source = ComicSource( _name!, key, _loadAccountConfig(), _loadCategoryData(), _loadCategoryComicsData(), _loadFavoriteData(), _loadExploreData(), _loadSearchData(), _parseSettings(), _parseLoadComicFunc(), _parseThumbnailLoader(), _parseLoadComicPagesFunc(), _parseImageLoadingConfigFunc(), _parseThumbnailLoadingConfigFunc(), filePath, url ?? "", version ?? "1.0.0", _parseCommentsLoader(), _parseSendCommentFunc(), _parseLikeFunc(), _parseVoteCommentFunc(), _parseLikeCommentFunc(), _parseIdMatch(), _parseTranslation(), _parseClickTagEvent(), _parseLinkHandler(), ); await source.loadData(); Future.delayed(const Duration(milliseconds: 50), () { JsEngine().runCode("ComicSource.sources.$_key.init()"); }); return source; } _checkKeyValidation() { // 仅允许数字和字母以及下划线 if (!_key!.contains(RegExp(r"^[a-zA-Z0-9_]+$"))) { throw ComicSourceParseException("key $_key is invalid"); } } bool _checkExists(String index) { return JsEngine().runCode("ComicSource.sources.$_key.$index !== null " "&& ComicSource.sources.$_key.$index !== undefined"); } dynamic _getValue(String index) { return JsEngine().runCode("ComicSource.sources.$_key.$index"); } AccountConfig? _loadAccountConfig() { if (!_checkExists("account")) { return null; } Future> login(account, pwd) async { try { await JsEngine().runCode(""" ComicSource.sources.$_key.account.login(${jsonEncode(account)}, ${jsonEncode(pwd)}) """); var source = ComicSource.find(_key!)!; source.data["account"] = [account, pwd]; source.saveData(); return const Res(true); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } } void logout() { JsEngine().runCode("ComicSource.sources.$_key.account.logout()"); } if(!_checkExists('account.loginWithWebview')) { return AccountConfig( login, null, _getValue("account.registerWebsite"), logout, null, ); } else { return AccountConfig( null, _getValue("account.loginWithWebview.url"), _getValue("account.registerWebsite"), logout, (url, title) { return JsEngine().runCode(""" ComicSource.sources.$_key.account.loginWithWebview.checkStatus( ${jsonEncode(url)}, ${jsonEncode(title)}) """); }, ); } } List _loadExploreData() { if (!_checkExists("explore")) { return const []; } var length = JsEngine().runCode("ComicSource.sources.$_key.explore.length"); var pages = []; for (int i = 0; i < length; i++) { final String title = _getValue("explore[$i].title"); final String type = _getValue("explore[$i].type"); Future>> Function()? loadMultiPart; Future>> Function(int page)? loadPage; Future>> Function(int index)? loadMixed; if (type == "singlePageWithMultiPart") { loadMultiPart = () async { try { var res = await JsEngine() .runCode("ComicSource.sources.$_key.explore[$i].load()"); return Res(List.from(res.keys .map((e) => ExplorePagePart( e, (res[e] as List) .map((e) => Comic.fromJson(e, _key!)) .toList(), null)) .toList())); } catch (e, s) { Log.error("Data Analysis", "$e\n$s"); return Res.error(e.toString()); } }; } else if (type == "multiPageComicList") { loadPage = (int page) async { try { var res = await JsEngine().runCode( "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})"); return Res( List.generate(res["comics"].length, (index) => Comic.fromJson(res["comics"][index], _key!)), subData: res["maxPage"]); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } else if (type == "multiPartPage") { loadMultiPart = () async { try { var res = await JsEngine() .runCode("ComicSource.sources.$_key.explore[$i].load()"); return Res( List.from( (res as List).map((e) { return ExplorePagePart( e['title'], (e['comics'] as List).map((e) { return Comic.fromJson(e, _key!); }).toList(), e['viewMore'], ); }), ), ); } catch (e, s) { Log.error("Data Analysis", "$e\n$s"); return Res.error(e.toString()); } }; } else if (type == 'mixed') { loadMixed = (index) async { try { var res = await JsEngine().runCode( "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})"); var list = []; for (var data in (res['data'] as List)) { if (data is List) { list.add(data.map((e) => Comic.fromJson(e, _key!)).toList()); } else if (data is Map) { list.add(ExplorePagePart( data['title'], (data['comics'] as List).map((e) { return Comic.fromJson(e, _key!); }).toList(), data['viewMore'], )); } } return Res(list, subData: res['maxPage']); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } pages.add(ExplorePageData( title, switch (type) { "singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart, "multiPartPage" => ExplorePageType.singlePageWithMultiPart, "multiPageComicList" => ExplorePageType.multiPageComicList, "mixed" => ExplorePageType.mixed, _ => throw ComicSourceParseException("Unknown explore page type $type") }, loadPage, loadMultiPart, loadMixed, )); } return pages; } CategoryData? _loadCategoryData() { var doc = _getValue("category"); if (doc?["title"] == null) { return null; } final String title = doc["title"]; final bool? enableRankingPage = doc["enableRankingPage"]; var categoryParts = []; for (var c in doc["parts"]) { final String name = c["name"]; final String type = c["type"]; final List tags = List.from(c["categories"]); final String itemType = c["itemType"]; List? categoryParams = ListOrNull.from(c["categoryParams"]); final String? groupParam = c["groupParam"]; if (groupParam != null) { categoryParams = List.filled(tags.length, groupParam); } if (type == "fixed") { categoryParts .add(FixedCategoryPart(name, tags, itemType, categoryParams)); } else if (type == "random") { categoryParts.add( RandomCategoryPart(name, tags, c["randomNumber"] ?? 1, itemType)); } } return CategoryData( title: title, categories: categoryParts, enableRankingPage: enableRankingPage ?? false, key: title); } CategoryComicsData? _loadCategoryComicsData() { if (!_checkExists("categoryComics")) return null; var options = []; for (var element in _getValue("categoryComics.optionList")) { LinkedHashMap map = LinkedHashMap(); for (var option in element["options"]) { if (option.isEmpty || !option.contains("-")) { continue; } var split = option.split("-"); var key = split.removeAt(0); var value = split.join("-"); map[key] = value; } options.add(CategoryComicsOptions( map, List.from(element["notShowWhen"] ?? []), element["showWhen"] == null ? null : List.from(element["showWhen"]))); } RankingData? rankingData; if (_checkExists("categoryComics.ranking")) { var options = {}; for (var option in _getValue("categoryComics.ranking.options")) { if (option.isEmpty || !option.contains("-")) { continue; } var split = option.split("-"); var key = split.removeAt(0); var value = split.join("-"); options[key] = value; } rankingData = RankingData(options, (option, page) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.categoryComics.ranking.load( ${jsonEncode(option)}, ${jsonEncode(page)}) """); return Res( List.generate(res["comics"].length, (index) => Comic.fromJson(res["comics"][index], _key!)), subData: res["maxPage"]); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }); } return CategoryComicsData(options, (category, param, options, page) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.categoryComics.load( ${jsonEncode(category)}, ${jsonEncode(param)}, ${jsonEncode(options)}, ${jsonEncode(page)} ) """); return Res( List.generate(res["comics"].length, (index) => Comic.fromJson(res["comics"][index], _key!)), subData: res["maxPage"]); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }, rankingData: rankingData); } SearchPageData? _loadSearchData() { if (!_checkExists("search")) return null; var options = []; for (var element in _getValue("search.optionList") ?? []) { LinkedHashMap map = LinkedHashMap(); for (var option in element["options"]) { if (option.isEmpty || !option.contains("-")) { continue; } var split = option.split("-"); var key = split.removeAt(0); var value = split.join("-"); map[key] = value; } options.add(SearchOptions(map, element["label"])); } return SearchPageData(options, (keyword, page, searchOption) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.search.load( ${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)}) """); return Res( List.generate(res["comics"].length, (index) => Comic.fromJson(res["comics"][index], _key!)), subData: res["maxPage"]); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }); } LoadComicFunc? _parseLoadComicFunc() { return (id) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.comic.loadInfo(${jsonEncode(id)}) """); if (res is! Map) throw "Invalid data"; res['comicId'] = id; res['sourceKey'] = _key; return Res(ComicDetails.fromJson(res)); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } LoadComicPagesFunc? _parseLoadComicPagesFunc() { return (id, ep) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.comic.loadEp(${jsonEncode(id)}, ${jsonEncode(ep)}) """); return Res(List.from(res["images"])); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } FavoriteData? _loadFavoriteData() { if (!_checkExists("favorites")) return null; final bool multiFolder = _getValue("favorites.multiFolder"); Future> retryZone(Future> Function() func) async { if (!ComicSource.find(_key!)!.isLogged) { return const Res.error("Not login"); } var res = await func(); if (res.error && res.errorMessage!.contains("Login expired")) { var reLoginRes = await ComicSource.find(_key!)!.reLogin(); if (!reLoginRes) { return const Res.error("Login expired and re-login failed"); } else { return func(); } } return res; } Future> addOrDelFavFunc(comicId, folderId, isAdding) async { func() async { try { await JsEngine().runCode(""" ComicSource.sources.$_key.favorites.addOrDelFavorite( ${jsonEncode(comicId)}, ${jsonEncode(folderId)}, ${jsonEncode(isAdding)}) """); return const Res(true); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } } return retryZone(func); } Future>> loadComic(int page, [String? folder]) async { Future>> func() async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.favorites.loadComics( ${jsonEncode(page)}, ${jsonEncode(folder)}) """); return Res( List.generate(res["comics"].length, (index) => Comic.fromJson(res["comics"][index], _key!)), subData: res["maxPage"]); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } } return retryZone(func); } Future>> Function([String? comicId])? loadFolders; Future> Function(String name)? addFolder; Future> Function(String key)? deleteFolder; if (multiFolder) { loadFolders = ([String? comicId]) async { Future>> func() async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.favorites.loadFolders(${jsonEncode(comicId)}) """); List? subData; if (res["favorited"] != null) { subData = List.from(res["favorited"]); } return Res(Map.from(res["folders"]), subData: subData); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } } return retryZone(func); }; addFolder = (name) async { try { await JsEngine().runCode(""" ComicSource.sources.$_key.favorites.addFolder(${jsonEncode(name)}) """); return const Res(true); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; deleteFolder = (key) async { try { await JsEngine().runCode(""" ComicSource.sources.$_key.favorites.deleteFolder(${jsonEncode(key)}) """); return const Res(true); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } return FavoriteData( key: _key!, title: _name!, multiFolder: multiFolder, loadComic: loadComic, loadFolders: loadFolders, addFolder: addFolder, deleteFolder: deleteFolder, addOrDelFavorite: addOrDelFavFunc, ); } CommentsLoader? _parseCommentsLoader() { if (!_checkExists("comic.loadComments")) return null; return (id, subId, page, replyTo) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.comic.loadComments( ${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)}) """); return Res( (res["comments"] as List).map((e) => Comment.fromJson(e)).toList(), subData: res["maxPage"]); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } SendCommentFunc? _parseSendCommentFunc() { if (!_checkExists("comic.sendComment")) return null; return (id, subId, content, replyTo) async { Future> func() async { try { await JsEngine().runCode(""" ComicSource.sources.$_key.comic.sendComment( ${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(content)}, ${jsonEncode(replyTo)}) """); return const Res(true); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } } var res = await func(); if (res.error && res.errorMessage!.contains("Login expired")) { var reLoginRes = await ComicSource.find(_key!)!.reLogin(); if (!reLoginRes) { return const Res.error("Login expired and re-login failed"); } else { return func(); } } return res; }; } GetImageLoadingConfigFunc? _parseImageLoadingConfigFunc() { if (!_checkExists("comic.onImageLoad")) { return null; } return (imageKey, comicId, ep) { return JsEngine().runCode(""" ComicSource.sources.$_key.comic.onImageLoad( ${jsonEncode(imageKey)}, ${jsonEncode(comicId)}, ${jsonEncode(ep)}) """) as Map; }; } GetThumbnailLoadingConfigFunc? _parseThumbnailLoadingConfigFunc() { if (!_checkExists("comic.onThumbnailLoad")) { return null; } return (imageKey) { var res = JsEngine().runCode(""" ComicSource.sources.$_key.comic.onThumbnailLoad(${jsonEncode(imageKey)}) """); if (res is! Map) { Log.error("Network", "function onThumbnailLoad return invalid data"); throw "function onThumbnailLoad return invalid data"; } return res as Map; }; } ComicThumbnailLoader? _parseThumbnailLoader() { if (!_checkExists("comic.loadThumbnail")) { return null; } return (id, next) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.comic.loadThumbnail(${jsonEncode(id)}, ${jsonEncode(next)}) """); return Res(List.from(res['thumbnails']), subData: res['next']); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } LikeOrUnlikeComicFunc? _parseLikeFunc() { if (!_checkExists("comic.likeComic")) { return null; } return (id, isLiking) async { try { await JsEngine().runCode(""" ComicSource.sources.$_key.comic.likeComic(${jsonEncode(id)}, ${jsonEncode(isLiking)}) """); return const Res(true); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } VoteCommentFunc? _parseVoteCommentFunc() { if (!_checkExists("comic.voteComment")) { return null; } return (id, subId, commentId, isUp, isCancel) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.comic.voteComment(${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(commentId)}, ${jsonEncode(isUp)}, ${jsonEncode(isCancel)}) """); return Res(res is num ? res.toInt() : 0); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } LikeCommentFunc? _parseLikeCommentFunc() { if (!_checkExists("comic.likeComment")) { return null; } return (id, subId, commentId, isLiking) async { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.comic.likeComment(${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(commentId)}, ${jsonEncode(isLiking)}) """); return Res(res is num ? res.toInt() : 0); } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e.toString()); } }; } Map _parseSettings() { return _getValue("settings") ?? {}; } RegExp? _parseIdMatch() { if (!_checkExists("comic.idMatch")) { return null; } return RegExp(_getValue("comic.idMatch")); } Map>? _parseTranslation() { if (!_checkExists("translation")) { return null; } var data = _getValue("translation"); var res = >{}; for (var e in data.entries) { res[e.key] = Map.from(e.value); } return res; } HandleClickTagEvent? _parseClickTagEvent() { if (!_checkExists("comic.onClickTag")) { return null; } return (namespace, tag) { var res = JsEngine().runCode(""" ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)}) """); var r = Map.from(res); r.removeWhere((key, value) => value == null); return Map.from(r); }; } LinkHandler? _parseLinkHandler() { if (!_checkExists("comic.link")) { return null; } List domains = List.from(_getValue("comic.link.domains")); linkToId(String link) { var res = JsEngine().runCode(""" ComicSource.sources.$_key.comic.link.linkToId(${jsonEncode(link)}) """); return res as String?; } return LinkHandler(domains, linkToId); } }