diff --git a/assets/translation.json b/assets/translation.json index 45079e2..8f8ef7c 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -389,7 +389,7 @@ "Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题", "Show single image on first page": "在首页显示单张图片", "Click to select an image": "点击选择一张图片", - "Source URL": "源地址", + "Repo URL": "仓库地址", "The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件", "Double tap to zoom": "双击缩放", "Clear Unfavorited": "清除未收藏", @@ -786,7 +786,7 @@ "Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題", "Show single image on first page": "在首頁顯示單張圖片", "Click to select an image": "點擊選擇一張圖片", - "Source URL": "源地址", + "Repo URL": "倉庫地址", "The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件", "Double tap to zoom": "雙擊縮放", "Clear Unfavorited": "清除未收藏", diff --git a/doc/comic_source.md b/doc/comic_source.md index 9d5d830..4ddecd1 100644 --- a/doc/comic_source.md +++ b/doc/comic_source.md @@ -9,13 +9,45 @@ Venera uses [flutter_qjs](https://github.com/wgh136/flutter_qjs) as js engine wh This document will describe how to write a comic source for Venera. -## Preparation +## Comic Source List + +Venera can display a list of comic sources in the app. + +You should provide a repository url to let the app load the comic source list. +The url should point to a JSON file that contains the list of comic sources. + +The JSON file should have the following format: + +```json +[ + { + "name": "Source Name", + "url": "https://example.com/source.js", + "filename": "Relative path to the source file", + "version": "1.0.0", + "description": "A brief description of the source" + } +] +``` + +Only one of `url` and `filename` should be provided. +The description field is optional. + +Currently, you can use the following repo url: +``` +https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/index.json +``` +The repo is maintained by the Venera team, and you can submit a pull request to add your comic source. + +## Create a Comic Source + +### Preparation - Install Venera. Using flutter to run the project is recommended since it's easier to debug. - An editor that supports javascript. - Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs). -## Start Writing +### Start Writing The template contains detailed comments and examples. You can refer to it when writing your own comic source. @@ -23,7 +55,7 @@ Here is a brief introduction to the template: > Note: Javascript api document is [here](js_api.md). -### Write basic information +#### Write basic information ```javascript class NewComicSource extends ComicSource { @@ -49,7 +81,7 @@ In this part, you need to do the following: - Change the class name to your source name. - Fill in the name, key, version, minAppVersion, and url fields. -### init function +#### init function ```javascript /** @@ -64,7 +96,7 @@ The function will be called when the source is initialized. You can do some init Remove this function if not used. -### Account +#### Account ```javascript // [Optional] account related @@ -140,7 +172,7 @@ In this part, you can implement login, logout, and register functions. Remove this part if not used. -### Explore page +#### Explore page ```javascript // explore page list @@ -185,7 +217,7 @@ There are three types of explore pages: - multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page. - mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button. -### Category Page +#### Category Page ```javascript // categories @@ -227,7 +259,7 @@ Category page is a static page that contains multiple parts, each part contains A comic source can only have one category page. -### Category Comics Page +#### Category Comics Page ```javascript /// category comic loading related @@ -280,7 +312,7 @@ When user clicks on a category, the category comics page will be displayed. This part is used to load comics of a category. -### Search +#### Search ```javascript /// search related @@ -339,7 +371,7 @@ This part is used to load search results. `load` and `loadNext` functions are used to load search results. If `load` function is implemented, `loadNext` function will be ignored. -### Favorites +#### Favorites ```javascript // favorite related @@ -411,7 +443,7 @@ This part is used to manage network favorites of the source. `load` and `loadNext` functions are used to load search results. If `load` function is implemented, `loadNext` function will be ignored. -### Comic Details +#### Comic Details ```javascript /// single comic related @@ -576,7 +608,7 @@ If `load` function is implemented, `loadNext` function will be ignored. This part is used to load comic details. -### Settings +#### Settings ```javascript /* @@ -635,7 +667,7 @@ This part is used to load comic details. This part is used to provide settings for the source. -### Translations +#### Translations ```javascript // [Optional] translations for the strings in this config diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 13ed776..a0403f2 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.4.4"; + final version = "1.4.5"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 5600664..fb9052a 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -189,7 +189,7 @@ class Settings with ChangeNotifier { 'customImageProcessing': defaultCustomImageProcessing, 'sni': true, 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese - 'comicSourceListUrl': defaultComicSourceUrl, + 'comicSourceListUrl': '', 'preloadImageCount': 4, 'followUpdatesFolder': null, 'initialPage': '0', @@ -233,5 +233,3 @@ function processImage(image, cid, eid, page, sourceKey) { return futureImage; } '''; - -const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json"; diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 812d6eb..9a0c100 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:isolate'; import 'package:flutter/widgets.dart' show ChangeNotifier; +import 'package:flutter_saf/flutter_saf.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; @@ -108,15 +109,42 @@ class LocalComic with HistoryMixin implements Comic { void read() { var history = HistoryManager().find(id, comicType); + int? firstDownloadedChapter; + int? firstDownloadedChapterGroup; + if (downloadedChapters.isNotEmpty && chapters != null) { + final chapters = this.chapters!; + if (chapters.isGrouped) { + for (int i=0; i Reader( type: comicType, cid: id, name: title, chapters: chapters, - initialChapter: history?.ep, + initialChapter: history?.ep ?? firstDownloadedChapter, initialPage: history?.page, - initialChapterGroup: history?.group, + initialChapterGroup: history?.group ?? firstDownloadedChapterGroup, history: history ?? History.fromModel( model: this, @@ -625,6 +653,7 @@ class LocalManager with ChangeNotifier { /// Deletes the directories in a separate isolate to avoid blocking the UI thread. static void _deleteDirectories(List directories) { Isolate.run(() async { + await SAFTaskWorker().init(); for (var dir in directories) { try { if (dir.existsSync()) { diff --git a/lib/init.dart b/lib/init.dart index 88549d9..48d40da 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -95,8 +95,7 @@ Future _checkAppUpdates() async { appdata.writeImplicitData(); ComicSourcePage.checkComicSourceUpdate(); if (appdata.settings['checkUpdateOnStart']) { - await Future.delayed(const Duration(milliseconds: 300)); - await checkUpdateUi(false); + await checkUpdateUi(false, true); } } diff --git a/lib/network/download.dart b/lib/network/download.dart index bb8c6d3..7ab6777 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -552,7 +552,7 @@ class _ImageDownloadWrapper { void start() async { int lastBytes = 0; try { - await for (var p in ImageDownloader.loadComicImage( + await for (var p in ImageDownloader.loadComicImageUnwrapped( image, task.source.key, task.comicId, chapter)) { if (isCancelled) { return; diff --git a/lib/network/images.dart b/lib/network/images.dart index 26189f7..74749ac 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -111,6 +111,11 @@ abstract class ImageDownloader { return stream.stream; } + static Stream loadComicImageUnwrapped( + String imageKey, String? sourceKey, String cid, String eid) { + return _loadComicImage(imageKey, sourceKey, cid, eid); + } + static Stream _loadComicImage( String imageKey, String? sourceKey, String cid, String eid) async* { final cacheKey = "$imageKey@$sourceKey@$cid@$eid"; diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 81b9622..d79a403 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -51,9 +51,7 @@ class ComicSourcePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: const _Body(), - ); + return Scaffold(body: const _Body()); } } @@ -87,10 +85,7 @@ class _BodyState extends State<_Body> { Widget build(BuildContext context) { return SmoothCustomScrollView( slivers: [ - SliverAppbar( - title: Text('Comic Source'.tl), - style: AppbarStyle.shadow, - ), + SliverAppbar(title: Text('Comic Source'.tl), style: AppbarStyle.shadow), buildCard(context), for (var source in ComicSource.all()) _SliverComicSource( @@ -109,9 +104,7 @@ class _BodyState extends State<_Body> { showConfirmDialog( context: App.rootContext, title: "Delete".tl, - content: "Delete comic source '@n' ?".tlParams({ - "n": source.name, - }), + content: "Delete comic source '@n' ?".tlParams({"n": source.name}), btnColor: context.colorScheme.error, onConfirm: () { var file = File(source.filePath); @@ -133,14 +126,16 @@ class _BodyState extends State<_Body> { title: const Text("Reload Configs"), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("cancel")), + onPressed: () => Navigator.pop(context), + child: const Text("cancel"), + ), TextButton( - onPressed: () async { - await ComicSourceManager().reload(); - App.forceRebuild(); - }, - child: const Text("continue")), + onPressed: () async { + await ComicSourceManager().reload(); + App.forceRebuild(); + }, + child: const Text("continue"), + ), ], ), ); @@ -157,8 +152,10 @@ class _BodyState extends State<_Body> { ); } - static Future update(ComicSource source, - [bool showLoading = true]) async { + static Future update( + ComicSource source, [ + bool showLoading = true, + ]) async { if (!source.url.isURL) { App.rootContext.showMessage(message: "Invalid url config"); return; @@ -174,8 +171,10 @@ class _BodyState extends State<_Body> { ); } try { - var res = await AppDio().get(source.url, - options: Options(responseType: ResponseType.plain)); + var res = await AppDio().get( + source.url, + options: Options(responseType: ResponseType.plain), + ); if (cancel) return; controller?.close(); await ComicSourceParser().parse(res.data!, source.filePath); @@ -192,12 +191,11 @@ class _BodyState extends State<_Body> { } Widget buildCard(BuildContext context) { - Widget buildButton( - {required Widget child, required VoidCallback onPressed}) { - return Button.normal( - onPressed: onPressed, - child: child, - ).fixHeight(32); + Widget buildButton({ + required Widget child, + required VoidCallback onPressed, + }) { + return Button.normal(onPressed: onPressed, child: child).fixHeight(32); } return SliverToBoxAdapter( @@ -213,12 +211,14 @@ class _BodyState extends State<_Body> { ), TextField( decoration: InputDecoration( - hintText: "URL", - border: const UnderlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - suffix: IconButton( - onPressed: () => handleAddSource(url), - icon: const Icon(Icons.check))), + hintText: "URL", + border: const UnderlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + suffix: IconButton( + onPressed: () => handleAddSource(url), + icon: const Icon(Icons.check), + ), + ), onChanged: (value) { url = value; }, @@ -245,10 +245,7 @@ class _BodyState extends State<_Body> { ), ListTile( title: Text("Help".tl), - trailing: buildButton( - onPressed: help, - child: Text("Open".tl), - ), + trailing: buildButton(onPressed: help, child: Text("Open".tl)), ), ListTile( title: Text("Check updates".tl), @@ -277,7 +274,8 @@ class _BodyState extends State<_Body> { void help() { launchUrlString( - "https://github.com/venera-app/venera/blob/master/doc/comic_source.md"); + "https://github.com/venera-app/venera/blob/master/doc/comic_source.md", + ); } Future handleAddSource(String url) async { @@ -288,11 +286,16 @@ class _BodyState extends State<_Body> { splits.removeWhere((element) => element == ""); var fileName = splits.last; bool cancel = false; - var controller = showLoadingDialog(App.rootContext, - onCancel: () => cancel = true, barrierDismissible: false); + var controller = showLoadingDialog( + App.rootContext, + onCancel: () => cancel = true, + barrierDismissible: false, + ); try { - var res = await AppDio() - .get(url, options: Options(responseType: ResponseType.plain)); + var res = await AppDio().get( + url, + options: Options(responseType: ResponseType.plain), + ); if (cancel) return; controller.close(); await addSource(res.data!, fileName); @@ -332,6 +335,12 @@ class _ComicSourceListState extends State<_ComicSourceList> { json = null; }); } + if (controller.text.isEmpty) { + setState(() { + json = []; + }); + return; + } var dio = AppDio(); try { var res = await dio.get(controller.text); @@ -343,8 +352,7 @@ class _ComicSourceListState extends State<_ComicSourceList> { json = jsonDecode(res.data!); }); } - } - catch(e) { + } catch (e) { context.showMessage(message: "Network error".tl); if (mounted) { setState(() { @@ -372,10 +380,7 @@ class _ComicSourceListState extends State<_ComicSourceList> { @override Widget build(BuildContext context) { - return PopUpWidgetScaffold( - title: "Comic Source".tl, - body: buildBody(), - ); + return PopUpWidgetScaffold(title: "Comic Source".tl, body: buildBody()); } Widget buildBody() { @@ -399,32 +404,36 @@ class _ComicSourceListState extends State<_ComicSourceList> { children: [ ListTile( leading: Icon(Icons.source_outlined), - title: Text("Source URL".tl), + title: Text("Repo URL".tl), ), TextField( controller: controller, decoration: InputDecoration( hintText: "URL", border: const UnderlineInputBorder(), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), ), onChanged: (value) { changed = true; }, ).paddingHorizontal(16).paddingBottom(8), - Text("The URL should point to a 'index.json' file".tl).paddingLeft(16), - Text("Do not report any issues related to sources to App repo.".tl).paddingLeft(16), + Text( + "The URL should point to a 'index.json' file".tl, + ).paddingLeft(16), + Text( + "Do not report any issues related to sources to App repo.".tl, + ).paddingLeft(16), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { - controller.text = defaultComicSourceUrl; - changed = true; + launchUrlString( + "https://github.com/venera-app/venera/blob/master/doc/comic_source.md", + ); }, - child: Text("Reset".tl), + child: Text("Help".tl), ), FilledButton.tonal( onPressed: load, @@ -440,7 +449,11 @@ class _ComicSourceListState extends State<_ComicSourceList> { } if (index == 1 && json == null) { - return Center(child: CircularProgressIndicator()); + return Center( + child: CircularProgressIndicator( + strokeWidth: 2, + ).fixWidth(24).fixHeight(24), + ); } index--; @@ -449,28 +462,28 @@ class _ComicSourceListState extends State<_ComicSourceList> { var action = currentKey.contains(key) ? const Icon(Icons.check, size: 20).paddingRight(8) : Button.filled( - child: Text("Add".tl), - onPressed: () async { - var fileName = json![index]["fileName"]; - var url = json![index]["url"]; - if (url == null || !(url.toString()).isURL) { - var listUrl = - appdata.settings['comicSourceListUrl'] as String; - if (listUrl - .replaceFirst("https://", "") - .replaceFirst("http://", "") - .contains("/")) { - url = - listUrl.substring(0, listUrl.lastIndexOf("/") + 1) + - fileName; - } else { - url = '$listUrl/$fileName'; - } - } - await widget.onAdd(url); - setState(() {}); - }, - ).fixHeight(32); + child: Text("Add".tl), + onPressed: () async { + var fileName = json![index]["fileName"]; + var url = json![index]["url"]; + if (url == null || !(url.toString()).isURL) { + var listUrl = + appdata.settings['comicSourceListUrl'] as String; + if (listUrl + .replaceFirst("https://", "") + .replaceFirst("http://", "") + .contains("/")) { + url = + listUrl.substring(0, listUrl.lastIndexOf("/") + 1) + + fileName; + } else { + url = '$listUrl/$fileName'; + } + } + await widget.onAdd(url); + setState(() {}); + }, + ).fixHeight(32); var description = json![index]["version"]; if (json![index]["description"] != null) { @@ -551,8 +564,7 @@ void _addAllPagesWithComicSource(ComicSource source) { !networkFavorites.contains(source.favoriteData!.key)) { networkFavorites.add(source.favoriteData!.key); } - if (source.searchPageData != null && - !searchPages.contains(source.key)) { + if (source.searchPageData != null && !searchPages.contains(source.key)) { searchPages.add(source.key); } @@ -594,15 +606,10 @@ class __EditFilePageState extends State<_EditFilePage> { @override Widget build(BuildContext context) { return Scaffold( - appBar: Appbar( - title: Text("Edit".tl), - ), + appBar: Appbar(title: Text("Edit".tl)), body: Column( children: [ - Container( - height: 0.6, - color: context.colorScheme.outlineVariant, - ), + Container(height: 0.6, color: context.colorScheme.outlineVariant), Expanded( child: CodeEditor( initialValue: current, @@ -643,9 +650,11 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { } void showUpdateDialog() async { - var text = ComicSourceManager().availableUpdates.entries.map((e) { - return "${ComicSource.find(e.key)!.name}: ${e.value}"; - }).join("\n"); + var text = ComicSourceManager().availableUpdates.entries + .map((e) { + return "${ComicSource.find(e.key)!.name}: ${e.value}"; + }) + .join("\n"); bool doUpdate = false; await showDialog( context: App.rootContext, @@ -783,10 +792,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> { child: ListTile( title: Row( children: [ - Text( - source.name, - style: ts.s18, - ), + Text(source.name, style: ts.s18), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( @@ -819,7 +825,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> { style: const TextStyle(fontSize: 13), ), ), - ).paddingLeft(4) + ).paddingLeft(4), ], ), trailing: Row( @@ -864,15 +870,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> { ), ), SliverToBoxAdapter( - child: Column( - children: buildSourceSettings().toList(), - ), - ), - SliverToBoxAdapter( - child: Column( - children: _buildAccount().toList(), - ), + child: Column(children: buildSourceSettings().toList()), ), + SliverToBoxAdapter(child: Column(children: _buildAccount().toList())), ], ); } @@ -898,8 +898,10 @@ class _SliverComicSourceState extends State<_SliverComicSource> { } } } else { - current = item.value['options'] - .firstWhere((e) => e['value'] == current)['text'] ?? + current = + item.value['options'].firstWhere( + (e) => e['value'] == current, + )['text'] ?? current; } yield ListTile( @@ -907,8 +909,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> { trailing: Select( current: (current as String).ts(source.key), values: (item.value['options'] as List) - .map((e) => - ((e['text'] ?? e['value']) as String).ts(source.key)) + .map( + (e) => ((e['text'] ?? e['value']) as String).ts(source.key), + ) .toList(), onTap: (i) { source.data['settings'][key] = @@ -936,8 +939,11 @@ class _SliverComicSourceState extends State<_SliverComicSource> { source.data['settings'][key] ?? item.value['default'] ?? ''; yield ListTile( title: Text((item.value['title'] as String).ts(source.key)), - subtitle: - Text(current, maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + current, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), trailing: IconButton( icon: const Icon(Icons.edit), onPressed: () { @@ -978,10 +984,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> { trailing: const Icon(Icons.arrow_right), onTap: () async { await context.to( - () => _LoginPage( - config: source.account!, - source: source, - ), + () => _LoginPage(config: source.account!, source: source), ); source.saveData(); setState(() {}); @@ -1027,9 +1030,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> { trailing: loading ? const SizedBox.square( dimension: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - ), + child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), ); @@ -1070,9 +1071,7 @@ class _LoginPageState extends State<_LoginPage> { @override Widget build(BuildContext context) { return Scaffold( - appBar: const Appbar( - title: Text(''), - ), + appBar: const Appbar(title: Text('')), body: Center( child: Container( padding: const EdgeInsets.all(16), @@ -1200,8 +1199,9 @@ class _LoginPageState extends State<_LoginPage> { setState(() { loading = true; }); - var cookies = - widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList(); + var cookies = widget.config.cookieFields! + .map((e) => _cookies[e] ?? '') + .toList(); widget.config.validateCookies!(cookies).then((value) { if (value) { widget.source.data['account'] = 'ok'; diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 664ec46..bbdf963 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -66,6 +66,11 @@ class _FavoritesPageState extends State { folder = data['name']; isNetwork = data['isNetwork'] ?? false; } + if (folder != null + && !isNetwork + && !LocalFavoritesManager().existsFolder(folder!)) { + folder = null; + } super.initState(); } diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index fe4be2b..90f9b35 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -40,6 +40,9 @@ class _ReaderImagesState extends State<_ReaderImages> { reader.images = images; reader.isLoading = false; inProgress = false; + Future.microtask(() { + reader.updateHistory(); + }); }); } catch (e) { setState(() { @@ -65,6 +68,9 @@ class _ReaderImagesState extends State<_ReaderImages> { reader.images = res.data; reader.isLoading = false; inProgress = false; + Future.microtask(() { + reader.updateHistory(); + }); }); } } @@ -233,7 +239,7 @@ class _GalleryModeState extends State<_GalleryMode> photoViewControllers[index] ??= PhotoViewController(); - if (reader.imagesPerPage == 1) { + if (reader.imagesPerPage == 1 || pageImages.length == 1) { return PhotoViewGalleryPageOptions( filterQuality: FilterQuality.medium, controller: photoViewControllers[index], diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 6b732ba..8a30c64 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -164,9 +164,6 @@ class _ReaderState extends State } mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; - Future.microtask(() { - updateHistory(); - }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); if (appdata.settings['enableTurnPageByVolumeKey']) { handleVolumeEvent(); @@ -267,7 +264,15 @@ class _ReaderState extends State history!.page = images?.length ?? 1; } else { /// Record the first image of the page - history!.page = (page - 1) * imagesPerPage + 1; + if (!showSingleImageOnFirstPage || imagesPerPage == 1) { + history!.page = (page - 1) * imagesPerPage + 1; + } else { + if (page == 1) { + history!.page = 1; + } else { + history!.page = (page - 2) * imagesPerPage + 2; + } + } } history!.maxPage = images?.length ?? 1; if (widget.chapters?.isGrouped ?? false) { @@ -349,7 +354,11 @@ abstract mixin class _ImagePerPageHandler { void initImagesPerPage(int initialPage) { _lastImagesPerPage = imagesPerPage; if (imagesPerPage != 1) { - page = (initialPage / imagesPerPage).ceil(); + if (showSingleImageOnFirstPage) { + page = ((initialPage - 1) / imagesPerPage).ceil() + 1; + } else { + page = (initialPage / imagesPerPage).ceil(); + } } } diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 6f8ae5d..a50b67c 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -96,10 +96,13 @@ Future checkUpdate() async { return false; } -Future checkUpdateUi([bool showMessageIfNoUpdate = true]) async { +Future checkUpdateUi([bool showMessageIfNoUpdate = true, bool delay = false]) async { try { var value = await checkUpdate(); if (value) { + if (delay) { + await Future.delayed(const Duration(seconds: 2)); + } showDialog( context: App.rootContext, builder: (context) { diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 52b890d..2d6e048 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -22,10 +22,12 @@ class DataSync with ChangeNotifier { } LocalFavoritesManager().addListener(onDataChanged); ComicSourceManager().addListener(onDataChanged); - Future.delayed(const Duration(seconds: 1), () { - var controller = WindowFrame.of(App.rootContext); - controller.addCloseListener(_handleWindowClose); - }); + if (App.isDesktop) { + Future.delayed(const Duration(seconds: 1), () { + var controller = WindowFrame.of(App.rootContext); + controller.addCloseListener(_handleWindowClose); + }); + } } void onDataChanged() { diff --git a/lib/utils/pdf.dart b/lib/utils/pdf.dart index 48da584..9e3f8ee 100644 --- a/lib/utils/pdf.dart +++ b/lib/utils/pdf.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; +import 'package:flutter_saf/flutter_saf.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/utils/image.dart'; @@ -74,6 +75,9 @@ Future _runIsolate( return Isolate.spawn( (sendPort) => overrideIO( () async { + if (App.isAndroid) { + await SAFTaskWorker().init(); + } var receivePort = ReceivePort(); sendPort.send(receivePort.sendPort); diff --git a/lib/utils/tags_translation.dart b/lib/utils/tags_translation.dart index f876283..d39a813 100644 --- a/lib/utils/tags_translation.dart +++ b/lib/utils/tags_translation.dart @@ -35,8 +35,10 @@ extension TagsTranslation on String{ /// 对tag进行处理后进行翻译: 代表'或'的分割符'|', namespace. static String _translateTags(String tag){ if (tag.contains('|')) { - var splits = tag.split(' | '); - return enTagsTranslations[splits[0]]??enTagsTranslations[splits[1]]??tag; + var splits = tag.split('|'); + return enTagsTranslations[splits[0].trim()] + ?? enTagsTranslations[splits[1].trim()] + ?? tag; } else if(tag.contains(':')) { var splits = tag.split(':'); if(_haveNamespace(splits[0])) { diff --git a/pubspec.lock b/pubspec.lock index 3325f12..46382b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1108,4 +1108,4 @@ packages: version: "0.0.12" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.0" + flutter: ">=3.32.4" diff --git a/pubspec.yaml b/pubspec.yaml index 6a7c76d..34048d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.4.4+144 +version: 1.4.5+145 environment: sdk: '>=3.8.0 <4.0.0' - flutter: 3.32.0 + flutter: 3.32.4 dependencies: flutter: