From b682d7d87b59a6a11a060b7041f6530bd463a20a Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 20 Oct 2024 11:11:47 +0800 Subject: [PATCH] improve html api; fix thumbnails loading api; add favoriteId api --- assets/init.js | 67 ++++++++++++- lib/components/message.dart | 1 + lib/foundation/comic_source/favorites.dart | 2 +- lib/foundation/comic_source/models.dart | 8 +- lib/foundation/comic_source/parser.dart | 16 ++- lib/foundation/js_engine.dart | 31 +++++- lib/foundation/local.dart | 3 + lib/pages/comic_page.dart | 67 ++++++++----- .../favorites/network_favorites_page.dart | 98 +++++++++++-------- 9 files changed, 219 insertions(+), 74 deletions(-) diff --git a/assets/init.js b/assets/init.js index 12bac7e..c485a50 100644 --- a/assets/init.js +++ b/assets/init.js @@ -242,9 +242,31 @@ function createUuid() { }); } +/** + * Generate a random integer between min and max + * @param min {number} + * @param max {number} + * @returns {number} + */ function randomInt(min, max) { return sendMessage({ method: 'random', + type: 'int', + min: min, + max: max + }); +} + +/** + * Generate a random double between min and max + * @param min {number} + * @param max {number} + * @returns {number} + */ +function randomDouble(min, max) { + return sendMessage({ + method: 'random', + type: 'double', min: min, max: max }); @@ -642,6 +664,45 @@ class HtmlElement { if(k == null) return null; return new HtmlElement(k); } + + /** + * Get class names of the element. + * @returns {string[]} An array of class names. + */ + get classNames() { + return sendMessage({ + method: "html", + function: "getClassNames", + key: this.key, + doc: this.doc, + }) + } + + /** + * Get id of the element. + * @returns {string | null} The id of the element. + */ + get id() { + return sendMessage({ + method: "html", + function: "getId", + key: this.key, + doc: this.doc, + }) + } + + /** + * Get local name of the element. + * @returns {string} The tag name of the element. + */ + get localName() { + return sendMessage({ + method: "html", + function: "getLocalName", + key: this.key, + doc: this.doc, + }) + } } class HtmlNode { @@ -727,9 +788,10 @@ let console = { * @param description {string} * @param maxPage {number?} * @param language {string?} + * @param favoriteId {string?} - Only set this field if the comic is from favorites page * @constructor */ -function Comic({id, title, subtitle, cover, tags, description, maxPage, language}) { +function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId}) { this.id = id; this.title = title; this.subtitle = subtitle; @@ -738,6 +800,7 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage, language this.description = description; this.maxPage = maxPage; this.language = language; + this.favoriteId = favoriteId; } /** @@ -749,7 +812,7 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage, language * @param chapters {Map | {} | null | undefined}} - key: chapter id, value: chapter title * @param isFavorite {boolean | null | undefined}} - favorite status. If the comic source supports multiple folders, this field should be null * @param subId {string?} - a param which is passed to comments api - * @param thumbnails {string[]? - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails + * @param thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails * @param recommend {Comic[]?} - related comics * @param commentCount {number?} * @param likesCount {number?} diff --git a/lib/components/message.dart b/lib/components/message.dart index b430d91..642dae6 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -57,6 +57,7 @@ class _ToastOverlay extends StatelessWidget { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500), maxLines: 3, + overflow: TextOverflow.ellipsis, ), if (trailing != null) trailing!.paddingLeft(8) ], diff --git a/lib/foundation/comic_source/favorites.dart b/lib/foundation/comic_source/favorites.dart index cc34d87..28cd93b 100644 --- a/lib/foundation/comic_source/favorites.dart +++ b/lib/foundation/comic_source/favorites.dart @@ -1,6 +1,6 @@ part of 'comic_source.dart'; -typedef AddOrDelFavFunc = Future> Function(String comicId, String folderId, bool isAdding); +typedef AddOrDelFavFunc = Future> Function(String comicId, String folderId, bool isAdding, String? favId); class FavoriteData{ final String key; diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 6d2b23a..a0d548e 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -58,6 +58,8 @@ class Comic { final String? language; + final String? favoriteId; + const Comic( this.title, this.cover, @@ -68,7 +70,7 @@ class Comic { this.sourceKey, this.maxPage, this.language, - ); + ): favoriteId = null; Map toJson() { return { @@ -81,6 +83,7 @@ class Comic { "sourceKey": sourceKey, "maxPage": maxPage, "language": language, + "favoriteId": favoriteId, }; } @@ -92,7 +95,8 @@ class Comic { tags = List.from(json["tags"] ?? []), description = json["description"] ?? "", maxPage = json["maxPage"], - language = json["language"]; + language = json["language"], + favoriteId = json["favoriteId"]; @override bool operator ==(Object other) { diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index 2e02e61..b114cc5 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -202,7 +202,7 @@ class ComicSourceParser { JsEngine().runCode("ComicSource.sources.$_key.account.logout()"); } - if(!_checkExists('account.loginWithWebview')) { + if (!_checkExists('account.loginWithWebview')) { return AccountConfig( login, null, @@ -378,7 +378,7 @@ class ComicSourceParser { CategoryComicsData? _loadCategoryComicsData() { if (!_checkExists("categoryComics")) return null; var options = []; - for (var element in _getValue("categoryComics.optionList")) { + for (var element in _getValue("categoryComics.optionList") ?? []) { LinkedHashMap map = LinkedHashMap(); for (var option in element["options"]) { if (option.isEmpty || !option.contains("-")) { @@ -528,7 +528,12 @@ class ComicSourceParser { return res; } - Future> addOrDelFavFunc(comicId, folderId, isAdding) async { + Future> addOrDelFavFunc( + String comicId, + String folderId, + bool isAdding, + String? favId, + ) async { func() async { try { await JsEngine().runCode(""" @@ -703,13 +708,13 @@ class ComicSourceParser { } ComicThumbnailLoader? _parseThumbnailLoader() { - if (!_checkExists("comic.loadThumbnail")) { + if (!_checkExists("comic.loadThumbnails")) { return null; } return (id, next) async { try { var res = await JsEngine().runCode(""" - ComicSource.sources.$_key.comic.loadThumbnail(${jsonEncode(id)}, ${jsonEncode(next)}) + ComicSource.sources.$_key.comic.loadThumbnails(${jsonEncode(id)}, ${jsonEncode(next)}) """); return Res(List.from(res['thumbnails']), subData: res['next']); } catch (e, s) { @@ -818,6 +823,7 @@ class ComicSourceParser { """); return res as String?; } + return LinkHandler(domains, linkToId); } } diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 3fe5c37..84da898 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -141,7 +141,11 @@ class JsEngine with _JSEngineApi { } case "random": { - return _randomInt(message["min"], message["max"]); + return _random( + message["min"] ?? 0, + message["max"] ?? 1, + message["type"], + ); } case "cookie": { @@ -232,7 +236,7 @@ mixin class _JSEngineApi { Object? handleHtmlCallback(Map data) { switch (data["function"]) { case "parse": - if(_documents.length > 2) { + if (_documents.length > 2) { _documents.remove(_documents.keys.first); } _documents[data["key"]] = DocumentWrapper.parse(data["data"]); @@ -276,6 +280,12 @@ mixin class _JSEngineApi { var docKey = data["key"]; _documents.remove(docKey); return null; + case "getClassNames": + return _documents[data["doc"]]!.getClassNames(data["key"]); + case "getId": + return _documents[data["doc"]]!.getId(data["key"]); + case "getLocalName": + return _documents[data["doc"]]!.getLocalName(data["key"]); } return null; } @@ -455,7 +465,10 @@ mixin class _JSEngineApi { : output.sublist(0, outputOffset); } - int _randomInt(int min, int max) { + num _random(num min, num max, String type) { + if (type == "double") { + return min + (max - min) * math.Random().nextDouble(); + } return (min + (max - min) * math.Random().nextDouble()).toInt(); } } @@ -568,4 +581,16 @@ class DocumentWrapper { } return null; } + + List getClassNames(int key) { + return (elements[key]).classes.toList(); + } + + String? getId(int key) { + return (elements[key]).id; + } + + String? getLocalName(int key) { + return (elements[key]).localName; + } } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index ee8284e..f5a036c 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -124,6 +124,9 @@ class LocalComic with HistoryMixin implements Comic { @override String? get language => null; + + @override + String? get favoriteId => null; } class LocalManager with ChangeNotifier { diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 03f92d3..06186e0 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -840,43 +840,38 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { late List thumbnails; - bool isInitialLoading = false; + bool isInitialLoading = true; String? next; + String? error; + @override void didChangeDependencies() { state = context.findAncestorStateOfType<_ComicPageState>()!; + loadNext(); thumbnails = List.from(state.comic.thumbnails ?? []); super.didChangeDependencies(); } - bool isLoading = false; - void loadNext() async { - if (state.comicSource.loadComicThumbnail == null || isLoading) return; + if (state.comicSource.loadComicThumbnail == null) return; if (!isInitialLoading && next == null) { return; } - setState(() { - isLoading = true; - }); var res = await state.comicSource.loadComicThumbnail!(state.comic.id, next); if (res.success) { thumbnails.addAll(res.data); next = res.subData; isInitialLoading = false; + } else { + error = res.errorMessage; } - setState(() { - isLoading = false; - }); + setState(() {}); } @override Widget build(BuildContext context) { - if (thumbnails.isEmpty) { - Future.microtask(loadNext); - } return SliverMainAxisGroup( slivers: [ SliverToBoxAdapter( @@ -887,7 +882,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { SliverGrid( delegate: SliverChildBuilderDelegate(childCount: thumbnails.length, (context, index) { - if (index == thumbnails.length - 1) { + if (index == thumbnails.length - 1 && error == null) { loadNext(); } return Padding( @@ -940,7 +935,19 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { childAspectRatio: 0.65, ), ), - if (isLoading) + if(error != null) + SliverToBoxAdapter( + child: Column( + children: [ + Text(error!), + Button.outlined( + onPressed: loadNext, + child: Text("Retry".tl), + ) + ], + ), + ) + else if (next != null || isInitialLoading) const SliverToBoxAdapter( child: ListLoadingIndicator(), ), @@ -1185,7 +1192,7 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> { isLoading = true; }); var res = await widget.comicSource.favoriteData! - .addOrDelFavorite!(widget.cid, '', !isFavorite); + .addOrDelFavorite!(widget.cid, '', !isFavorite, null); if (res.success) { widget.onFavorite(!isFavorite); context.pop(); @@ -1272,16 +1279,32 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> { ), ), Center( - child: FilledButton( - onPressed: () { + child: Button.filled( + isLoading: isLoading, + onPressed: () async { if (selected == null) { return; } - widget.comicSource.favoriteData!.addOrDelFavorite!( - widget.cid, selected!, !addedFolders.contains(selected!)); - context.pop(); + setState(() { + isLoading = true; + }); + var res = await widget.comicSource.favoriteData!.addOrDelFavorite!( + widget.cid, + selected!, + !addedFolders.contains(selected!), + null, + ); + if (res.success) { + context.showMessage(message: "Success".tl); + context.pop(); + } else { + context.showMessage(message: res.errorMessage!); + setState(() { + isLoading = false; + }); + } }, - child: addedFolders.contains(selected!) + child: selected != null && addedFolders.contains(selected!) ? Text("Remove".tl) : Text("Add".tl), ).paddingVertical(8), diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index 99f2071..5f21ad7 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -1,8 +1,11 @@ part of 'favorites_page.dart'; -// TODO: Add a menu option to delete a comic from favorites - -Future _deleteComic(String cid, String? fid, String sourceKey) async { +Future _deleteComic( + String cid, + String? fid, + String sourceKey, + String? favId, +) async { var source = ComicSource.find(sourceKey); if (source == null) { return false; @@ -31,6 +34,7 @@ Future _deleteComic(String cid, String? fid, String sourceKey) async { cid, fid ?? '', false, + favId, ); if (res.success) { context.showMessage(message: "Deleted".tl); @@ -115,7 +119,12 @@ class _NormalFavoritePage extends StatelessWidget { icon: Icons.delete_outline, text: "Remove".tl, onClick: () async { - var res = await _deleteComic(comic.id, null, comic.sourceKey); + var res = await _deleteComic( + comic.id, + null, + comic.sourceKey, + comic.favoriteId, + ); if (res) { comicListKey.currentState!.remove(comic); } @@ -291,14 +300,16 @@ class _MultiFolderFavoritesPageState extends State<_MultiFolderFavoritesPage> { ), onPressed: () { showDialog( - context: context, - builder: (context) { - return _CreateFolderDialog( - widget.data, - () => setState(() { - _loading = true; - })); - }); + context: context, + builder: (context) { + return _CreateFolderDialog( + widget.data, + () => setState(() { + _loading = true; + }), + ); + }, + ); }, ), ), @@ -382,7 +393,7 @@ class _FolderTile extends StatelessWidget { return StatefulBuilder(builder: (context, setState) { return ContentDialog( title: "Delete".tl, - content: Text("Are you sure you want to delete this folder?".tl), + content: Text("Are you sure you want to delete this folder?".tl).paddingHorizontal(16), actions: [ Button.filled( isLoading: loading, @@ -448,36 +459,37 @@ class _CreateFolderDialogState extends State<_CreateFolderDialog> { height: 10, ), if (loading) - const SizedBox( - child: Center( - child: CircularProgressIndicator(), - ), + Center( + child: const CircularProgressIndicator( + strokeWidth: 2, + ).fixWidth(24).fixHeight(24), ) else SizedBox( - height: 35, - child: Center( - child: TextButton( - onPressed: () { + height: 35, + child: Center( + child: TextButton( + onPressed: () { + setState(() { + loading = true; + }); + widget.data.addFolder!(controller.text).then((b) { + if (b.error) { + context.showMessage(message: b.errorMessage!); setState(() { - loading = true; + loading = false; }); - widget.data.addFolder!(controller.text).then((b) { - if (b.error) { - context.showMessage(message: b.errorMessage!); - setState(() { - loading = false; - }); - } else { - context.pop(); - context.showMessage( - message: "Created successfully".tl); - widget.updateState(); - } - }); - }, - child: Text("Submit".tl)), - )) + } else { + context.pop(); + context.showMessage(message: "Created successfully".tl); + widget.updateState(); + } + }); + }, + child: Text("Submit".tl), + ), + ), + ) ], ); } @@ -501,6 +513,9 @@ class _FavoriteFolder extends StatelessWidget { leadingSliver: SliverAppbar( title: Text(title), ), + errorLeading: Appbar( + title: Text(title), + ), loadPage: (i) => data.loadComic(i, folderID), menuBuilder: (comic) { return [ @@ -508,7 +523,12 @@ class _FavoriteFolder extends StatelessWidget { icon: Icons.delete_outline, text: "Remove".tl, onClick: () async { - var res = await _deleteComic(comic.id, null, comic.sourceKey); + var res = await _deleteComic( + comic.id, + null, + comic.sourceKey, + comic.favoriteId, + ); if (res) { comicListKey.currentState!.remove(comic); }