initial commit

This commit is contained in:
nyne
2024-09-29 16:17:03 +08:00
commit f08c5cccb9
196 changed files with 16761 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
part of comic_source;
class CategoryData {
/// The title is displayed in the tab bar.
final String title;
/// 当使用中文语言时, 英文的分类标签将在构建页面时被翻译为中文
final List<BaseCategoryPart> categories;
final bool enableRankingPage;
final String key;
final List<CategoryButtonData> buttons;
/// Data class for building category page.
const CategoryData({
required this.title,
required this.categories,
required this.enableRankingPage,
required this.key,
this.buttons = const [],
});
}
class CategoryButtonData {
final String label;
final void Function() onTap;
const CategoryButtonData({
required this.label,
required this.onTap,
});
}
abstract class BaseCategoryPart {
String get title;
List<String> get categories;
List<String>? get categoryParams => null;
bool get enableRandom;
String get categoryType;
/// Data class for building a part of category page.
const BaseCategoryPart();
}
class FixedCategoryPart extends BaseCategoryPart {
@override
final List<String> categories;
@override
bool get enableRandom => false;
@override
final String title;
@override
final String categoryType;
@override
final List<String>? categoryParams;
/// A [BaseCategoryPart] that show fixed tags on category page.
const FixedCategoryPart(this.title, this.categories, this.categoryType,
[this.categoryParams]);
}
class RandomCategoryPart extends BaseCategoryPart {
final List<String> tags;
final int randomNumber;
@override
final String title;
@override
bool get enableRandom => true;
@override
final String categoryType;
List<String> _categories() {
if (randomNumber >= tags.length) {
return tags;
}
return tags.sublist(math.Random().nextInt(tags.length - randomNumber));
}
@override
List<String> get categories => _categories();
/// A [BaseCategoryPart] that show random tags on category page.
const RandomCategoryPart(
this.title, this.tags, this.randomNumber, this.categoryType);
}
class RandomCategoryPartWithRuntimeData extends BaseCategoryPart {
final Iterable<String> Function() loadTags;
final int randomNumber;
@override
final String title;
@override
bool get enableRandom => true;
@override
final String categoryType;
static final random = math.Random();
List<String> _categories() {
var tags = loadTags();
if (randomNumber >= tags.length) {
return tags.toList();
}
final start = random.nextInt(tags.length - randomNumber);
var res = List.filled(randomNumber, '');
int index = -1;
for (var s in tags) {
index++;
if (start > index) {
continue;
} else if (index == start + randomNumber) {
break;
}
res[index - start] = s;
}
return res;
}
@override
List<String> get categories => _categories();
/// A [BaseCategoryPart] that show random tags on category page.
RandomCategoryPartWithRuntimeData(
this.title, this.loadTags, this.randomNumber, this.categoryType);
}
CategoryData getCategoryDataWithKey(String key) {
for (var source in ComicSource.sources) {
if (source.categoryData?.key == key) {
return source.categoryData!;
}
}
throw "Unknown category key $key";
}

View File

@@ -0,0 +1,540 @@
library comic_source;
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/utils/ext.dart';
import '../js_engine.dart';
import '../log.dart';
part 'category.dart';
part 'favorites.dart';
part 'parser.dart';
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
typedef LoginFunction = Future<Res<bool>> Function(String, String);
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
String id, String? ep);
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
String id, String? subId, int page, String? replyTo);
typedef SendCommentFunc = Future<Res<bool>> Function(
String id, String? subId, String content, String? replyTo);
typedef GetImageLoadingConfigFunc = Map<String, dynamic> Function(
String imageKey, String comicId, String epId)?;
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
String imageKey)?;
class ComicSource {
static List<ComicSource> sources = [];
static ComicSource? find(String key) =>
sources.firstWhereOrNull((element) => element.key == key);
static ComicSource? fromIntKey(int key) =>
sources.firstWhereOrNull((element) => element.key.hashCode == key);
static Future<void> init() async {
final path = "${App.dataPath}/comic_source";
if (!(await Directory(path).exists())) {
Directory(path).create();
return;
}
await for (var entity in Directory(path).list()) {
if (entity is File && entity.path.endsWith(".js")) {
try {
var source = await ComicSourceParser()
.parse(await entity.readAsString(), entity.absolute.path);
sources.add(source);
} catch (e, s) {
Log.error("ComicSource", "$e\n$s");
}
}
}
}
static Future reload() async {
sources.clear();
JsEngine().runCode("ComicSource.sources = {};");
await init();
}
/// Name of this source.
final String name;
/// Identifier of this source.
final String key;
int get intKey {
return key.hashCode;
}
/// Account config.
final AccountConfig? account;
/// Category data used to build a static category tags page.
final CategoryData? categoryData;
/// Category comics data used to build a comics page with a category tag.
final CategoryComicsData? categoryComicsData;
/// Favorite data used to build favorite page.
final FavoriteData? favoriteData;
/// Explore pages.
final List<ExplorePageData> explorePages;
/// Search page.
final SearchPageData? searchPageData;
/// Settings.
final List<SettingItem> settings;
/// Load comic info.
final LoadComicFunc? loadComicInfo;
/// Load comic pages.
final LoadComicPagesFunc? loadComicPages;
final Map<String, dynamic> Function(
String imageKey, String comicId, String epId)? getImageLoadingConfig;
final Map<String, dynamic> Function(String imageKey)?
getThumbnailLoadingConfig;
final String? matchBriefIdReg;
var data = <String, dynamic>{};
bool get isLogin => data["account"] != null;
final String filePath;
final String url;
final String version;
final CommentsLoader? commentsLoader;
final SendCommentFunc? sendCommentFunc;
final RegExp? idMatcher;
Future<void> loadData() async {
var file = File("${App.dataPath}/comic_source/$key.data");
if (await file.exists()) {
data = Map.from(jsonDecode(await file.readAsString()));
}
}
bool _isSaving = false;
bool _haveWaitingTask = false;
Future<void> saveData() async {
if (_haveWaitingTask) return;
while (_isSaving) {
_haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 20));
_haveWaitingTask = false;
}
_isSaving = true;
var file = File("${App.dataPath}/comic_source/$key.data");
if (!await file.exists()) {
await file.create(recursive: true);
}
await file.writeAsString(jsonEncode(data));
_isSaving = false;
}
Future<bool> reLogin() async {
if (data["account"] == null) {
return false;
}
final List accountData = data["account"];
var res = await account!.login!(accountData[0], accountData[1]);
if (res.error) {
Log.error("Failed to re-login", res.errorMessage ?? "Error");
}
return !res.error;
}
ComicSource(
this.name,
this.key,
this.account,
this.categoryData,
this.categoryComicsData,
this.favoriteData,
this.explorePages,
this.searchPageData,
this.settings,
this.loadComicInfo,
this.loadComicPages,
this.getImageLoadingConfig,
this.getThumbnailLoadingConfig,
this.matchBriefIdReg,
this.filePath,
this.url,
this.version,
this.commentsLoader,
this.sendCommentFunc)
: idMatcher = null;
ComicSource.unknown(this.key)
: name = "Unknown",
account = null,
categoryData = null,
categoryComicsData = null,
favoriteData = null,
explorePages = [],
searchPageData = null,
settings = [],
loadComicInfo = null,
loadComicPages = null,
getImageLoadingConfig = null,
getThumbnailLoadingConfig = null,
matchBriefIdReg = null,
filePath = "",
url = "",
version = "",
commentsLoader = null,
sendCommentFunc = null,
idMatcher = null;
}
class AccountConfig {
final LoginFunction? login;
final FutureOr<void> Function(BuildContext)? onLogin;
final String? loginWebsite;
final String? registerWebsite;
final void Function() logout;
final bool allowReLogin;
final List<AccountInfoItem> infoItems;
const AccountConfig(
this.login, this.loginWebsite, this.registerWebsite, this.logout,
{this.onLogin})
: allowReLogin = true,
infoItems = const [];
}
class AccountInfoItem {
final String title;
final String Function()? data;
final void Function()? onTap;
final WidgetBuilder? builder;
AccountInfoItem({required this.title, this.data, this.onTap, this.builder});
}
class LoadImageRequest {
String url;
Map<String, String> headers;
LoadImageRequest(this.url, this.headers);
}
class ExplorePageData {
final String title;
final ExplorePageType type;
final ComicListBuilder? loadPage;
final Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
/// return a `List` contains `List<Comic>` or `ExplorePagePart`
final Future<Res<List<Object>>> Function(int index)? loadMixed;
final WidgetBuilder? overridePageBuilder;
ExplorePageData(this.title, this.type, this.loadPage, this.loadMultiPart)
: loadMixed = null,
overridePageBuilder = null;
}
class ExplorePagePart {
final String title;
final List<Comic> comics;
/// If this is not null, the [ExplorePagePart] will show a button to jump to new page.
///
/// Value of this field should match the following format:
/// - search:keyword
/// - category:categoryName
///
/// End with `@`+`param` if the category has a parameter.
final String? viewMore;
const ExplorePagePart(this.title, this.comics, this.viewMore);
}
enum ExplorePageType {
multiPageComicList,
singlePageWithMultiPart,
mixed,
override,
}
typedef SearchFunction = Future<Res<List<Comic>>> Function(
String keyword, int page, List<String> searchOption);
class SearchPageData {
/// If this is not null, the default value of search options will be first element.
final List<SearchOptions>? searchOptions;
final Widget Function(BuildContext, List<String> initialValues, void Function(List<String>))?
customOptionsBuilder;
final Widget Function(String keyword, List<String> options)?
overrideSearchResultBuilder;
final SearchFunction? loadPage;
final bool enableLanguageFilter;
final bool enableTagsSuggestions;
const SearchPageData(this.searchOptions, this.loadPage)
: enableLanguageFilter = false,
customOptionsBuilder = null,
overrideSearchResultBuilder = null,
enableTagsSuggestions = false;
}
class SearchOptions {
final LinkedHashMap<String, String> options;
final String label;
const SearchOptions(this.options, this.label);
String get defaultValue => options.keys.first;
}
class SettingItem {
final String name;
final String iconName;
final SettingType type;
final List<String>? options;
const SettingItem(this.name, this.iconName, this.type, this.options);
}
enum SettingType {
switcher,
selector,
input,
}
class Comic {
final String title;
final String cover;
final String id;
final String? subTitle;
final List<String>? tags;
final String description;
final String sourceKey;
const Comic(this.title, this.cover, this.id, this.subTitle, this.tags, this.description, this.sourceKey);
Map<String, dynamic> toJson() {
return {
"title": title,
"cover": cover,
"id": id,
"subTitle": subTitle,
"tags": tags,
"description": description,
"sourceKey": sourceKey,
};
}
Comic.fromJson(Map<String, dynamic> json, this.sourceKey)
: title = json["title"],
subTitle = json["subTitle"] ?? "",
cover = json["cover"],
id = json["id"],
tags = List<String>.from(json["tags"] ?? []),
description = json["description"] ?? "";
}
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 Map<String, String>? chapters;
final List<String>? thumbnails;
final Future<Res<List<String>>> Function(String id, int page)?
thumbnailLoader;
final int thumbnailMaxPage;
final List<Comic>? suggestions;
final String sourceKey;
final String comicId;
final bool? isFavorite;
final String? subId;
const ComicDetails(
this.title,
this.subTitle,
this.cover,
this.description,
this.tags,
this.chapters,
this.thumbnails,
this.thumbnailLoader,
this.thumbnailMaxPage,
this.suggestions,
this.sourceKey,
this.comicId,
{this.isFavorite,
this.subId});
Map<String, dynamic> toJson() {
return {
"title": title,
"subTitle": subTitle,
"cover": cover,
"description": description,
"tags": tags,
"chapters": chapters,
"sourceKey": sourceKey,
"comicId": comicId,
"isFavorite": isFavorite,
"subId": subId,
};
}
static Map<String, List<String>> _generateMap(Map<String, 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 = Map<String, String>.from(json["chapters"]),
sourceKey = json["sourceKey"],
comicId = json["comicId"],
thumbnails = null,
thumbnailLoader = null,
thumbnailMaxPage = 0,
suggestions = null,
isFavorite = json["isFavorite"],
subId = json["subId"];
@override
HistoryType get historyType => HistoryType(sourceKey.hashCode);
@override
String get id => comicId;
}
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
String category, String? param, List<String> options, int page);
class CategoryComicsData {
/// options
final List<CategoryComicsOptions> options;
/// [category] is the one clicked by the user on the category page.
/// if [BaseCategoryPart.categoryParams] is not null, [param] will be not null.
///
/// [Res.subData] should be maxPage or null if there is no limit.
final CategoryComicsLoader load;
final RankingData? rankingData;
const CategoryComicsData(this.options, this.load, {this.rankingData});
}
class RankingData {
final Map<String, String> options;
final Future<Res<List<Comic>>> Function(String option, int page) load;
const RankingData(this.options, this.load);
}
class CategoryComicsOptions {
/// Use a [LinkedHashMap] to describe an option list.
/// key is for loading comics, value is the name displayed on screen.
/// Default value will be the first of the Map.
final LinkedHashMap<String, String> options;
/// If [notShowWhen] contains category's name, the option will not be shown.
final List<String> notShowWhen;
final List<String>? showWhen;
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
}
class Comment {
final String userName;
final String? avatar;
final String content;
final String? time;
final int? replyCount;
final String? id;
const Comment(this.userName, this.avatar, this.content, this.time,
this.replyCount, this.id);
}

View File

@@ -0,0 +1,50 @@
part of 'comic_source.dart';
typedef AddOrDelFavFunc = Future<Res<bool>> Function(String comicId, String folderId, bool isAdding);
class FavoriteData{
final String key;
final String title;
final bool multiFolder;
final Future<Res<List<Comic>>> Function(int page, [String? folder]) loadComic;
/// key-id, value-name
///
/// if comicId is not null, Res.subData is the folders that the comic is in
final Future<Res<Map<String, String>>> Function([String? comicId])? loadFolders;
/// A value of null disables this feature
final Future<Res<bool>> Function(String key)? deleteFolder;
/// A value of null disables this feature
final Future<Res<bool>> Function(String name)? addFolder;
/// A value of null disables this feature
final String? allFavoritesId;
final AddOrDelFavFunc? addOrDelFavorite;
const FavoriteData({
required this.key,
required this.title,
required this.multiFolder,
required this.loadComic,
this.loadFolders,
this.deleteFolder,
this.addFolder,
this.allFavoritesId,
this.addOrDelFavorite});
}
FavoriteData getFavoriteData(String key){
var source = ComicSource.find(key) ?? (throw "Unknown source key: $key");
return source.favoriteData!;
}
FavoriteData? getFavoriteDataOrNull(String key){
var source = ComicSource.find(key);
return source?.favoriteData;
}

View File

@@ -0,0 +1,652 @@
part of 'comic_source.dart';
bool compareSemVer(String ver1, String ver2) {
ver1 = ver1.replaceFirst("-", ".");
ver2 = ver2.replaceFirst("-", ".");
List<String> v1 = ver1.split('.');
List<String> 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<ComicSource> createAndParse(String js, String fileName) async{
if(!fileName.endsWith("js")){
fileName = "$fileName.js";
}
var file = File("${App.dataPath}/comic_source/$fileName");
if(file.existsSync()){
int i = 0;
while(file.existsSync()){
file = File("${App.dataPath}/comic_source/$fileName($i).js");
i++;
}
}
await file.writeAsString(js);
try{
return await parse(js, file.path);
} catch (e) {
await file.delete();
rethrow;
}
}
Future<ComicSource> 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");
var matchBriefIdRegex = JsEngine().runCode("this['temp'].comic.matchBriefIdRegex");
if(minAppVersion != null){
if(compareSemVer(minAppVersion, App.version.split('-').first)){
throw ComicSourceParseException("minAppVersion $minAppVersion is required");
}
}
for(var source in ComicSource.sources){
if(source.key == key){
throw ComicSourceParseException("key($key) already exists");
}
}
_key = key;
_checkKeyValidation();
JsEngine().runCode("""
ComicSource.sources.$_key = this['temp'];
""");
final account = _loadAccountConfig();
final explorePageData = _loadExploreData();
final categoryPageData = _loadCategoryData();
final categoryComicsData =
_loadCategoryComicsData();
final searchData = _loadSearchData();
final loadComicFunc = _parseLoadComicFunc();
final loadComicPagesFunc = _parseLoadComicPagesFunc();
final getImageLoadingConfigFunc = _parseImageLoadingConfigFunc();
final getThumbnailLoadingConfigFunc = _parseThumbnailLoadingConfigFunc();
final favoriteData = _loadFavoriteData();
final commentsLoader = _parseCommentsLoader();
final sendCommentFunc = _parseSendCommentFunc();
var source = ComicSource(
_name!,
key,
account,
categoryPageData,
categoryComicsData,
favoriteData,
explorePageData,
searchData,
[],
loadComicFunc,
loadComicPagesFunc,
getImageLoadingConfigFunc,
getThumbnailLoadingConfigFunc,
matchBriefIdRegex,
filePath,
url ?? "",
version ?? "1.0.0",
commentsLoader,
sendCommentFunc);
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<Res<bool>> login(account, pwd) async {
try {
await JsEngine().runCode("""
ComicSource.sources.$_key.account.login(${jsonEncode(account)},
${jsonEncode(pwd)})
""");
var source = ComicSource.sources
.firstWhere((element) => element.key == _key);
source.data["account"] = <String>[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()");
}
return AccountConfig(
login,
_getValue("account.login.website"),
_getValue("account.registerWebsite"),
logout
);
}
List<ExplorePageData> _loadExploreData() {
if (!_checkExists("explore")) {
return const [];
}
var length = JsEngine().runCode("ComicSource.sources.$_key.explore.length");
var pages = <ExplorePageData>[];
for (int i=0; i<length; i++) {
final String title = _getValue("explore[$i].title");
final String type = _getValue("explore[$i].type");
Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
Future<Res<List<Comic>>> Function(int page)? loadPage;
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<Comic>((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());
}
};
}
pages.add(ExplorePageData(
title,
switch (type) {
"singlePageWithMultiPart" =>
ExplorePageType.singlePageWithMultiPart,
"multiPageComicList" => ExplorePageType.multiPageComicList,
_ =>
throw ComicSourceParseException("Unknown explore page type $type")
},
loadPage,
loadMultiPart));
}
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 = <BaseCategoryPart>[];
for (var c in doc["parts"]) {
final String name = c["name"];
final String type = c["type"];
final List<String> tags = List.from(c["categories"]);
final String itemType = c["itemType"];
final List<String>? categoryParams =
c["categoryParams"] == null ? null : List.from(c["categoryParams"]);
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 = <CategoryComicsOptions>[];
for (var element in _getValue("categoryComics.optionList")) {
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
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 = <String, String>{};
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 = <SearchOptions>[];
for (var element in _getValue("search.optionList") ?? []) {
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
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)})
""");
var tags = <String, List<String>>{};
(res["tags"] as Map<String, dynamic>?)
?.forEach((key, value) => tags[key] = List.from(value ?? const []));
return Res(ComicDetails(
res["title"],
res["subTitle"],
res["cover"],
res["description"],
tags,
res["chapters"] == null ? null : Map.from(res["chapters"]),
ListOrNull.from(res["thumbnails"]),
// TODO: implement thumbnailLoader
null,
res["thumbnailMaxPage"] ?? 1,
(res["recommend"] as List?)
?.map((e) => Comic.fromJson(e, _key!))
.toList(),
_key!,
id,
isFavorite: res["isFavorite"],
subId: res["subId"],));
} 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<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async{
if(!ComicSource.find(_key!)!.isLogin){
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<Res<bool>> 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<bool>.error(e.toString());
}
}
return retryZone(func);
}
Future<Res<List<Comic>>> loadComic(int page, [String? folder]) async {
Future<Res<List<Comic>>> 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<Res<Map<String, String>>> Function([String? comicId])? loadFolders;
Future<Res<bool>> Function(String name)? addFolder;
Future<Res<bool>> Function(String key)? deleteFolder;
if(multiFolder) {
loadFolders = ([String? comicId]) async {
Future<Res<Map<String, String>>> func() async{
try {
var res = await JsEngine().runCode("""
ComicSource.sources.$_key.favorites.loadFolders(${jsonEncode(comicId)})
""");
List<String>? 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(
e["userName"], e["avatar"], e["content"], e["time"], e["replyCount"], e["id"].toString()
)).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<Res<bool>> 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<String, dynamic>;
};
}
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<String, dynamic>;
};
}
}