diff --git a/assets/translation.json b/assets/translation.json index bbce784..9017678 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -310,7 +310,11 @@ "Check for updates on startup": "启动时检查更新", "Start Time": "开始时间", "End Time": "结束时间", - "Custom": "自定义" + "Custom": "自定义", + "Reset": "重置", + "Tags": "标签", + "Authors": "作者", + "Comics": "漫画" }, "zh_TW": { "Home": "首頁", @@ -623,6 +627,10 @@ "Check for updates on startup": "啟動時檢查更新", "Start Time": "開始時間", "End Time": "結束時間", - "Custom": "自定義" + "Custom": "自定義", + "Reset": "重置", + "Tags": "標籤", + "Authors": "作者", + "Comics": "漫畫" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 648e841..c194073 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -149,7 +149,7 @@ class _Settings with ChangeNotifier { 'enableDnsOverrides': false, 'dnsOverrides': {}, 'enableCustomImageProcessing': false, - 'customImageProcessing': _defaultCustomImageProcessing, + 'customImageProcessing': defaultCustomImageProcessing, 'sni': true, 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese }; @@ -169,15 +169,20 @@ class _Settings with ChangeNotifier { } } -const _defaultCustomImageProcessing = ''' +const defaultCustomImageProcessing = ''' /** * Process an image - * @param image {ArayBuffer} - The image to process + * @param image {ArrayBuffer} - The image to process * @param cid {string} - The comic ID * @param eid {string} - The episode ID - * @returns {Promise} - The processed image + * @param page {number} - The page number + * @param sourceKey {string} - The source key + * @returns {Promise | {image: Promise, onCancel: () => void}} - The processed image */ -async function processImage(image, cid, eid) { +function processImage(image, cid, eid, page, sourceKey) { + let image = new Promise((resolve, reject) => { + resolve(image); + }); return image; } '''; diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index e0b8f2c..6fb1d29 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -78,7 +78,13 @@ abstract class BaseImageProvider> while (data == null && !stop) { try { - data = await load(chunkEvents); + data = await load(chunkEvents, () { + if (stop) { + throw const _ImageLoadingStopException(); + } + }); + } on _ImageLoadingStopException { + rethrow; } catch (e) { if (e.toString().contains("Invalid Status Code: 404")) { rethrow; @@ -100,7 +106,7 @@ abstract class BaseImageProvider> } if (stop) { - throw Exception("Image loading is stopped"); + throw const _ImageLoadingStopException(); } if (data!.isEmpty) { @@ -127,6 +133,8 @@ abstract class BaseImageProvider> } rethrow; } + } on _ImageLoadingStopException { + rethrow; } catch (e, s) { scheduleMicrotask(() { PaintingBinding.instance.imageCache.evict(key); @@ -138,7 +146,10 @@ abstract class BaseImageProvider> } } - Future load(StreamController chunkEvents); + Future load( + StreamController chunkEvents, + void Function() checkStop, + ); String get key; @@ -159,3 +170,7 @@ abstract class BaseImageProvider> } typedef FileDecoderCallback = Future Function(Uint8List); + +class _ImageLoadingStopException implements Exception { + const _ImageLoadingStopException(); +} diff --git a/lib/foundation/image_provider/cached_image.dart b/lib/foundation/image_provider/cached_image.dart index be5cd4a..6a4fd54 100644 --- a/lib/foundation/image_provider/cached_image.dart +++ b/lib/foundation/image_provider/cached_image.dart @@ -1,4 +1,4 @@ -import 'dart:async' show Future, StreamController; +import 'dart:async' show Future; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/network/images.dart'; @@ -26,9 +26,10 @@ class CachedImageProvider static const _kMaxLoadingCount = 8; @override - Future load(StreamController chunkEvents) async { + Future load(chunkEvents, checkStop) async { while(loadingCount > _kMaxLoadingCount) { await Future.delayed(const Duration(milliseconds: 100)); + checkStop(); } loadingCount++; try { @@ -37,6 +38,7 @@ class CachedImageProvider return file.readAsBytes(); } await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { + checkStop(); chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: progress.currentBytes, expectedTotalBytes: progress.totalBytes, diff --git a/lib/foundation/image_provider/history_image_provider.dart b/lib/foundation/image_provider/history_image_provider.dart index 4e2475a..d188b34 100644 --- a/lib/foundation/image_provider/history_image_provider.dart +++ b/lib/foundation/image_provider/history_image_provider.dart @@ -1,4 +1,4 @@ -import 'dart:async' show Future, StreamController; +import 'dart:async' show Future; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/foundation/local.dart'; @@ -17,7 +17,7 @@ class HistoryImageProvider final History history; @override - Future load(StreamController chunkEvents) async { + Future load(chunkEvents, checkStop) async { var url = history.cover; if (!url.contains('/')) { var localComic = LocalManager().find(history.id, history.type); @@ -27,6 +27,7 @@ class HistoryImageProvider var comicSource = history.type.comicSource ?? (throw "Comic source not found."); var comic = await comicSource.loadComicInfo!(history.id); + checkStop(); url = comic.data.cover; history.cover = url; HistoryManager().addHistory(history); @@ -36,6 +37,7 @@ class HistoryImageProvider history.type.sourceKey, history.id, )) { + checkStop(); chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: progress.currentBytes, expectedTotalBytes: progress.totalBytes, diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart index ec273f1..0ccea9d 100644 --- a/lib/foundation/image_provider/image_favorites_provider.dart +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -1,5 +1,4 @@ import 'dart:async' show Future, StreamController; -import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -29,30 +28,36 @@ class ImageFavoritesProvider String get eid => imageFavorite.eid; @override - Future load(StreamController? chunkEvents) async { + Future load( + StreamController? chunkEvents, + void Function()? checkStop, + ) async { var imageKey = imageFavorite.imageKey; var localImage = await getImageFromLocal(); + checkStop?.call(); if (localImage != null) { return localImage; } var cacheImage = await readFromCache(); + checkStop?.call(); if (cacheImage != null) { return cacheImage; } var gotImageKey = false; if (imageKey == "") { imageKey = await getImageKey(); + checkStop?.call(); gotImageKey = true; } Uint8List image; try { - image = await getImageFromNetwork(imageKey, chunkEvents); + image = await getImageFromNetwork(imageKey, chunkEvents, checkStop); } catch (e) { if (gotImageKey) { rethrow; } else { imageKey = await getImageKey(); - image = await getImageFromNetwork(imageKey, chunkEvents); + image = await getImageFromNetwork(imageKey, chunkEvents, checkStop); } } await writeToCache(image); @@ -106,9 +111,13 @@ class ImageFavoritesProvider } Future getImageFromNetwork( - String imageKey, StreamController? chunkEvents) async { + String imageKey, + StreamController? chunkEvents, + void Function()? checkStop, + ) async { await for (var progress in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { + checkStop?.call(); if (chunkEvents != null) { chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: progress.currentBytes, diff --git a/lib/foundation/image_provider/local_comic_image.dart b/lib/foundation/image_provider/local_comic_image.dart index 591b840..860894a 100644 --- a/lib/foundation/image_provider/local_comic_image.dart +++ b/lib/foundation/image_provider/local_comic_image.dart @@ -1,4 +1,4 @@ -import 'dart:async' show Future, StreamController; +import 'dart:async' show Future; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/foundation/local.dart'; @@ -16,7 +16,7 @@ class LocalComicImageProvider final LocalComic comic; @override - Future load(StreamController chunkEvents) async { + Future load(chunkEvents, checkStop) async { File? file = comic.coverFile; if(! await file.exists()) { file = null; @@ -49,6 +49,7 @@ class LocalComicImageProvider if(file == null) { throw "Error: Cover not found."; } + checkStop(); var data = await file.readAsBytes(); if(data.isEmpty) { throw "Exception: Empty file(${file.path})."; diff --git a/lib/foundation/image_provider/local_favorite_image.dart b/lib/foundation/image_provider/local_favorite_image.dart index feae47f..11fbb2e 100644 --- a/lib/foundation/image_provider/local_favorite_image.dart +++ b/lib/foundation/image_provider/local_favorite_image.dart @@ -1,4 +1,4 @@ -import 'dart:async' show Future, StreamController; +import 'dart:async' show Future; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/foundation/app.dart'; @@ -28,7 +28,7 @@ class LocalFavoriteImageProvider } @override - Future load(StreamController chunkEvents) async { + Future load(chunkEvents, checkStop) async { var sourceKey = ComicSource.fromIntKey(intKey)?.key; var fileName = key.hashCode.toString(); var file = File(FilePath.join(App.dataPath, 'favorite_cover', fileName)); @@ -37,7 +37,9 @@ class LocalFavoriteImageProvider } else { await file.create(recursive: true); } + checkStop(); await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey)) { + checkStop(); chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: progress.currentBytes, expectedTotalBytes: progress.totalBytes, diff --git a/lib/foundation/image_provider/reader_image.dart b/lib/foundation/image_provider/reader_image.dart index 0afc72a..9c93a95 100644 --- a/lib/foundation/image_provider/reader_image.dart +++ b/lib/foundation/image_provider/reader_image.dart @@ -1,4 +1,4 @@ -import 'dart:async' show Future, StreamController; +import 'dart:async' show Future; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_qjs/flutter_qjs.dart'; @@ -12,7 +12,7 @@ import 'package:venera/foundation/appdata.dart'; class ReaderImageProvider extends BaseImageProvider { /// Image provider for normal image. - const ReaderImageProvider(this.imageKey, this.sourceKey, this.cid, this.eid); + const ReaderImageProvider(this.imageKey, this.sourceKey, this.cid, this.eid, this.page); final String imageKey; @@ -22,8 +22,10 @@ class ReaderImageProvider final String eid; + final int page; + @override - Future load(StreamController chunkEvents) async { + Future load(chunkEvents, checkStop) async { Uint8List? imageBytes; if (imageKey.startsWith('file://')) { var file = File(imageKey); @@ -35,6 +37,7 @@ class ReaderImageProvider } else { await for (var event in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { + checkStop(); chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: event.currentBytes, expectedTotalBytes: event.totalBytes, @@ -60,14 +63,57 @@ class ReaderImageProvider })() '''); if (func is JSInvokable) { - var result = await func.invoke([imageBytes, cid, eid]); - func.free(); + var result = func.invoke([imageBytes, cid, eid, page, sourceKey]); if (result is Uint8List) { - return result; + imageBytes = result; + } else if (result is Future) { + var futureResult = await result; + if (futureResult is Uint8List) { + imageBytes = futureResult; + } + } else if (result is Map) { + var image = result['image']; + if (image is Uint8List) { + imageBytes = image; + } else if (image is Future) { + JSInvokable? onCancel; + if (result['onCancel'] is JSInvokable) { + onCancel = result['onCancel']; + } + if (onCancel == null) { + var futureImage = await image; + if (futureImage is Uint8List) { + imageBytes = futureImage; + } + } else { + dynamic futureImage; + image.then((value) { + futureImage = value; + futureImage ??= Uint8List(0); + }); + while (futureImage == null) { + try { + checkStop(); + } + catch(e) { + onCancel.invoke([]); + onCancel.free(); + func.free(); + rethrow; + } + await Future.delayed(Duration(milliseconds: 50)); + } + if (futureImage is Uint8List) { + imageBytes = futureImage; + } + } + onCancel?.free(); + } } + func.free(); } } - return imageBytes; + return imageBytes!; } @override diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index e8543b1..51768e6 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -224,7 +224,7 @@ class _ImageFavoritesPhotoViewState extends State { onClick: () async { var temp = images[currentPage]; var imageProvider = ImageFavoritesProvider(temp); - var data = await imageProvider.load(null); + var data = await imageProvider.load(null, null); var fileType = detectFileType(data); var fileName = "${currentPage + 1}.${fileType.ext}"; await saveFile(filename: fileName, data: data); diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index d1a76ee..11ef91a 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -673,6 +673,7 @@ ImageProvider _createImageProviderFromKey( reader.type.comicSource?.key, reader.cid, reader.eid, + reader.page, ); } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 3249ae3..00453d3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -650,6 +650,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { reader.type.comicSource!.key, reader.cid, reader.eid, + reader.page, ); } return InkWell( diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 778a215..79f3fa8 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -131,9 +131,10 @@ class _ReaderSettingsState extends State { "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" .tl, ).toSliver(), - _PopupWindowSetting( + _CallbackSetting( title: "Custom Image Processing".tl, - builder: () => _CustomImageProcessing(), + callback: () => context.to(() => _CustomImageProcessing()), + actionTitle: "Edit".tl, ).toSliver(), ], ); @@ -163,10 +164,25 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> { super.dispose(); } + int resetKey = 0; + @override Widget build(BuildContext context) { - return PopUpWidgetScaffold( - title: "Custom Image Processing".tl, + return Scaffold( + appBar: Appbar( + title: Text("Custom Image Processing".tl), + actions: [ + TextButton( + onPressed: () { + current = defaultCustomImageProcessing; + appdata.settings['customImageProcessing'] = current; + resetKey++; + setState(() {}); + }, + child: Text("Reset".tl), + ) + ], + ), body: Column( children: [ _SwitchSetting( @@ -182,6 +198,7 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> { ), child: SizedBox.expand( child: CodeEditor( + key: ValueKey(resetKey), initialValue: appdata.settings['customImageProcessing'], onChanged: (value) { current = value;