mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
initial commit
This commit is contained in:
153
lib/foundation/comic_source/category.dart
Normal file
153
lib/foundation/comic_source/category.dart
Normal 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";
|
||||
}
|
540
lib/foundation/comic_source/comic_source.dart
Normal file
540
lib/foundation/comic_source/comic_source.dart
Normal 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);
|
||||
}
|
50
lib/foundation/comic_source/favorites.dart
Normal file
50
lib/foundation/comic_source/favorites.dart
Normal 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;
|
||||
}
|
652
lib/foundation/comic_source/parser.dart
Normal file
652
lib/foundation/comic_source/parser.dart
Normal 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>;
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user