add star rating, network cache, advanced search option, loginWithCookies, loadNext; fix some minor issues

This commit is contained in:
nyne
2024-10-25 22:51:23 +08:00
parent b682d7d87b
commit 897f92f4c9
27 changed files with 1420 additions and 319 deletions

View File

@@ -27,6 +27,10 @@ part 'models.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);
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
String? next);
typedef LoginFunction = Future<Res<bool>> Function(String, String);
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
@@ -40,7 +44,7 @@ typedef CommentsLoader = Future<Res<List<Comment>>> Function(
typedef SendCommentFunc = Future<Res<bool>> Function(
String id, String? subId, String content, String? replyTo);
typedef GetImageLoadingConfigFunc = Map<String, dynamic> Function(
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
String imageKey, String comicId, String epId)?;
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
String imageKey)?;
@@ -64,6 +68,9 @@ typedef VoteCommentFunc = Future<Res<int?>> Function(
typedef HandleClickTagEvent = Map<String, String> Function(
String namespace, String tag);
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
class ComicSource {
static final List<ComicSource> _sources = [];
@@ -163,8 +170,7 @@ class ComicSource {
/// Load comic pages.
final LoadComicPagesFunc? loadComicPages;
final Map<String, dynamic> Function(
String imageKey, String comicId, String epId)? getImageLoadingConfig;
final GetImageLoadingConfigFunc? getImageLoadingConfig;
final Map<String, dynamic> Function(String imageKey)?
getThumbnailLoadingConfig;
@@ -203,6 +209,8 @@ class ComicSource {
final bool enableTagsTranslate;
final StarRatingFunc? starRatingFunc;
Future<void> loadData() async {
var file = File("${App.dataPath}/comic_source/$key.data");
if (await file.exists()) {
@@ -270,6 +278,7 @@ class ComicSource {
this.linkHandler,
this.enableTagsSuggestions,
this.enableTagsTranslate,
this.starRatingFunc,
);
}
@@ -288,12 +297,21 @@ class AccountConfig {
final bool Function(String url, String title)? checkLoginStatus;
final void Function()? onLoginWithWebviewSuccess;
final List<String>? cookieFields;
final Future<bool> Function(List<String>)? validateCookies;
const AccountConfig(
this.login,
this.loginWebsite,
this.registerWebsite,
this.logout,
this.checkLoginStatus,
this.onLoginWithWebviewSuccess,
this.cookieFields,
this.validateCookies,
) : allowReLogin = true,
infoItems = const [];
}
@@ -322,6 +340,8 @@ class ExplorePageData {
final ComicListBuilder? loadPage;
final ComicListBuilderWithNext? loadNext;
final Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
/// return a `List` contains `List<Comic>` or `ExplorePagePart`
@@ -331,6 +351,7 @@ class ExplorePageData {
this.title,
this.type,
this.loadPage,
this.loadNext,
this.loadMultiPart,
this.loadMixed,
);
@@ -388,9 +409,13 @@ class SearchOptions {
final String label;
const SearchOptions(this.options, this.label);
final String type;
String get defaultValue => options.keys.first;
final String? defaultVal;
const SearchOptions(this.options, this.label, this.type, this.defaultVal);
String get defaultValue => defaultVal ?? options.keys.first;
}
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
@@ -401,7 +426,7 @@ class CategoryComicsData {
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.
@@ -415,9 +440,12 @@ class CategoryComicsData {
class RankingData {
final Map<String, String> options;
final Future<Res<List<Comic>>> Function(String option, int page) load;
final Future<Res<List<Comic>>> Function(String option, int page)? load;
const RankingData(this.options, this.load);
final Future<Res<List<Comic>>> Function(String option, String? next)?
loadWithNext;
const RankingData(this.options, this.load, this.loadWithNext);
}
class CategoryComicsOptions {
@@ -434,11 +462,10 @@ class CategoryComicsOptions {
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
}
class LinkHandler {
final List<String> domains;
final String? Function(String url) linkToId;
const LinkHandler(this.domains, this.linkToId);
}
}

View File

@@ -1,20 +1,26 @@
part of 'comic_source.dart';
typedef AddOrDelFavFunc = Future<Res<bool>> Function(String comicId, String folderId, bool isAdding, String? favId);
typedef AddOrDelFavFunc = Future<Res<bool>> Function(
String comicId, String folderId, bool isAdding, String? favId);
class FavoriteData{
class FavoriteData {
final String key;
final String title;
final bool multiFolder;
final Future<Res<List<Comic>>> Function(int page, [String? folder]) loadComic;
final Future<Res<List<Comic>>> Function(int page, [String? folder])?
loadComic;
final Future<Res<List<Comic>>> Function(String? next, [String? folder])?
loadNext;
/// 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;
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;
@@ -32,19 +38,21 @@ class FavoriteData{
required this.title,
required this.multiFolder,
required this.loadComic,
required this.loadNext,
this.loadFolders,
this.deleteFolder,
this.addFolder,
this.allFavoritesId,
this.addOrDelFavorite});
this.addOrDelFavorite,
});
}
FavoriteData getFavoriteData(String key){
FavoriteData getFavoriteData(String key) {
var source = ComicSource.find(key) ?? (throw "Unknown source key: $key");
return source.favoriteData!;
}
FavoriteData? getFavoriteDataOrNull(String key){
FavoriteData? getFavoriteDataOrNull(String key) {
var source = ComicSource.find(key);
return source?.favoriteData;
}
}

View File

@@ -7,9 +7,9 @@ class Comment {
final String? time;
final int? replyCount;
final String? id;
final int? score;
int? score;
final bool? isLiked;
final int? voteStatus; // 1: upvote, -1: downvote, 0: none
int? voteStatus; // 1: upvote, -1: downvote, 0: none
static String? parseTime(dynamic value) {
if (value == null) return null;
@@ -60,6 +60,9 @@ class Comic {
final String? favoriteId;
/// 0-5
final double? stars;
const Comic(
this.title,
this.cover,
@@ -70,7 +73,7 @@ class Comic {
this.sourceKey,
this.maxPage,
this.language,
): favoriteId = null;
): favoriteId = null, stars = null;
Map<String, dynamic> toJson() {
return {
@@ -96,7 +99,8 @@ class Comic {
description = json["description"] ?? "",
maxPage = json["maxPage"],
language = json["language"],
favoriteId = json["favoriteId"];
favoriteId = json["favoriteId"],
stars = (json["stars"] as num?)?.toDouble();
@override
bool operator ==(Object other) {
@@ -151,6 +155,11 @@ class ComicDetails with HistoryMixin {
final String? url;
final double? stars;
@override
final int? maxPage;
static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) {
var res = <String, List<String>>{};
map.forEach((key, value) {
@@ -182,7 +191,9 @@ class ComicDetails with HistoryMixin {
uploader = json["uploader"],
uploadTime = json["uploadTime"],
updateTime = json["updateTime"],
url = json["url"];
url = json["url"],
stars = (json["stars"] as num?)?.toDouble(),
maxPage = json["maxPage"];
Map<String, dynamic> toJson() {
return {

View File

@@ -150,6 +150,7 @@ class ComicSourceParser {
_parseLinkHandler(),
_getValue("search.enableTagsSuggestions") ?? false,
_getValue("comic.enableTagsTranslate") ?? false,
_parseStarRatingFunc(),
);
await source.loadData();
@@ -182,48 +183,77 @@ class ComicSourceParser {
return null;
}
Future<Res<bool>> login(account, pwd) async {
try {
await JsEngine().runCode("""
Future<Res<bool>> Function(String account, String pwd)? login;
if(_checkExists("account.login")) {
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"] = <String>[account, pwd];
source.saveData();
return const Res(true);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
}
var source = ComicSource.find(_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()");
}
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("""
bool Function(String url, String title)? checkLoginStatus;
void Function()? onLoginSuccess;
if (_checkExists('account.loginWithWebview')) {
checkLoginStatus = (url, title) {
return JsEngine().runCode("""
ComicSource.sources.$_key.account.loginWithWebview.checkStatus(
${jsonEncode(url)}, ${jsonEncode(title)})
""");
},
);
};
if (_checkExists('account.loginWithWebview.onLoginSuccess')) {
onLoginSuccess = () {
JsEngine().runCode("""
ComicSource.sources.$_key.account.loginWithWebview.onLoginSuccess()
""");
};
}
}
Future<bool> Function(List<String>)? validateCookies;
if (_checkExists('account.loginWithCookies?.validate')) {
validateCookies = (cookies) async {
try {
var res = await JsEngine().runCode("""
ComicSource.sources.$_key.account.loginWithCookies.validate(${jsonEncode(cookies)})
""");
return res;
} catch (e, s) {
Log.error("Network", "$e\n$s");
return false;
}
};
}
return AccountConfig(
login,
_getValue("account.loginWithWebview?.url"),
_getValue("account.registerWebsite"),
logout,
checkLoginStatus,
onLoginSuccess,
ListOrNull.from(_getValue("account.loginWithCookies?.fields")),
validateCookies,
);
}
List<ExplorePageData> _loadExploreData() {
@@ -237,6 +267,7 @@ class ComicSourceParser {
final String type = _getValue("explore[$i].type");
Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
Future<Res<List<Comic>>> Function(int page)? loadPage;
Future<Res<List<Comic>>> Function(String? next)? loadNext;
Future<Res<List<Object>>> Function(int index)? loadMixed;
if (type == "singlePageWithMultiPart") {
loadMultiPart = () async {
@@ -257,19 +288,36 @@ class ComicSourceParser {
}
};
} else if (type == "multiPageComicList") {
loadPage = (int page) async {
try {
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})");
return Res(
if (_checkExists("explore[$i].load")) {
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 {
loadNext = (next) async {
try {
var res = await JsEngine().runCode(
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})");
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());
}
};
subData: res["next"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
}
};
}
} else if (type == "multiPartPage") {
loadMultiPart = () async {
try {
@@ -330,6 +378,7 @@ class ComicSourceParser {
throw ComicSourceParseException("Unknown explore page type $type")
},
loadPage,
loadNext,
loadMultiPart,
loadMixed,
));
@@ -406,21 +455,44 @@ class ComicSourceParser {
var value = split.join("-");
options[key] = value;
}
rankingData = RankingData(options, (option, page) async {
try {
var res = await JsEngine().runCode("""
Future<Res<List<Comic>>> Function(String option, int page)? load;
Future<Res<List<Comic>>> Function(String option, String? next)?
loadWithNext;
if (_checkExists("categoryComics.ranking.load")) {
load = (option, page) async {
try {
var res = await JsEngine().runCode("""
ComicSource.sources.$_key.categoryComics.ranking.load(
${jsonEncode(option)}, ${jsonEncode(page)})
""");
return Res(
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 {
loadWithNext = (option, next) async {
try {
var res = await JsEngine().runCode("""
ComicSource.sources.$_key.categoryComics.ranking.loadWithNext(
${jsonEncode(option)}, ${jsonEncode(next)})
""");
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());
}
});
subData: res["next"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
}
};
}
rankingData = RankingData(options, load, loadWithNext);
}
return CategoryComicsData(options, (category, param, options, page) async {
try {
@@ -457,7 +529,12 @@ class ComicSourceParser {
var value = split.join("-");
map[key] = value;
}
options.add(SearchOptions(map, element["label"]));
options.add(SearchOptions(
map,
element["label"],
element['type'] ?? 'select',
element['default'] == null ? null : jsonEncode(element['default']),
));
}
return SearchPageData(options, (keyword, page, searchOption) async {
try {
@@ -550,24 +627,53 @@ class ComicSourceParser {
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("""
Future<Res<List<Comic>>> Function(int page, [String? folder])? loadComic;
Future<Res<List<Comic>>> Function(String? next, [String? folder])? loadNext;
if (_checkExists("favorites.loadComic")) {
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(
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);
};
}
if (_checkExists("favorites.loadNext")) {
loadNext = (String? next, [String? folder]) async {
Future<Res<List<Comic>>> func() async {
try {
var res = await JsEngine().runCode("""
ComicSource.sources.$_key.favorites.loadNext(
${jsonEncode(next)}, ${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());
subData: res["next"],
);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
}
}
}
return retryZone(func);
return retryZone(func);
};
}
Future<Res<Map<String, String>>> Function([String? comicId])? loadFolders;
@@ -625,6 +731,7 @@ class ComicSourceParser {
title: _name!,
multiFolder: multiFolder,
loadComic: loadComic,
loadNext: loadNext,
loadFolders: loadFolders,
addFolder: addFolder,
deleteFolder: deleteFolder,
@@ -683,11 +790,15 @@ class ComicSourceParser {
if (!_checkExists("comic.onImageLoad")) {
return null;
}
return (imageKey, comicId, ep) {
return JsEngine().runCode("""
return (imageKey, comicId, ep) async {
var res = JsEngine().runCode("""
ComicSource.sources.$_key.comic.onImageLoad(
${jsonEncode(imageKey)}, ${jsonEncode(comicId)}, ${jsonEncode(ep)})
""") as Map<String, dynamic>;
""");
if (res is Future) {
return await res;
}
return res;
};
}
@@ -826,4 +937,21 @@ class ComicSourceParser {
return LinkHandler(domains, linkToId);
}
StarRatingFunc? _parseStarRatingFunc() {
if (!_checkExists("comic.starRating")) {
return null;
}
return (id, rating) async {
try {
await JsEngine().runCode("""
ComicSource.sources.$_key.comic.starRating(${jsonEncode(id)}, ${jsonEncode(rating)})
""");
return const Res(true);
} catch (e, s) {
Log.error("Network", "$e\n$s");
return Res.error(e.toString());
}
};
}
}