Improve image loading

This commit is contained in:
2025-01-16 17:51:43 +08:00
parent 5d1d62e157
commit c640e6bfbf
13 changed files with 144 additions and 35 deletions

View File

@@ -310,7 +310,11 @@
"Check for updates on startup": "启动时检查更新", "Check for updates on startup": "启动时检查更新",
"Start Time": "开始时间", "Start Time": "开始时间",
"End Time": "结束时间", "End Time": "结束时间",
"Custom": "自定义" "Custom": "自定义",
"Reset": "重置",
"Tags": "标签",
"Authors": "作者",
"Comics": "漫画"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -623,6 +627,10 @@
"Check for updates on startup": "啟動時檢查更新", "Check for updates on startup": "啟動時檢查更新",
"Start Time": "開始時間", "Start Time": "開始時間",
"End Time": "結束時間", "End Time": "結束時間",
"Custom": "自定義" "Custom": "自定義",
"Reset": "重置",
"Tags": "標籤",
"Authors": "作者",
"Comics": "漫畫"
} }
} }

View File

@@ -149,7 +149,7 @@ class _Settings with ChangeNotifier {
'enableDnsOverrides': false, 'enableDnsOverrides': false,
'dnsOverrides': {}, 'dnsOverrides': {},
'enableCustomImageProcessing': false, 'enableCustomImageProcessing': false,
'customImageProcessing': _defaultCustomImageProcessing, 'customImageProcessing': defaultCustomImageProcessing,
'sni': true, 'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
}; };
@@ -169,15 +169,20 @@ class _Settings with ChangeNotifier {
} }
} }
const _defaultCustomImageProcessing = ''' const defaultCustomImageProcessing = '''
/** /**
* Process an image * Process an image
* @param image {ArayBuffer} - The image to process * @param image {ArrayBuffer} - The image to process
* @param cid {string} - The comic ID * @param cid {string} - The comic ID
* @param eid {string} - The episode ID * @param eid {string} - The episode ID
* @returns {Promise<ArrayBuffer>} - The processed image * @param page {number} - The page number
* @param sourceKey {string} - The source key
* @returns {Promise<ArrayBuffer> | {image: Promise<ArrayBuffer>, 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; return image;
} }
'''; ''';

View File

@@ -78,7 +78,13 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
while (data == null && !stop) { while (data == null && !stop) {
try { try {
data = await load(chunkEvents); data = await load(chunkEvents, () {
if (stop) {
throw const _ImageLoadingStopException();
}
});
} on _ImageLoadingStopException {
rethrow;
} catch (e) { } catch (e) {
if (e.toString().contains("Invalid Status Code: 404")) { if (e.toString().contains("Invalid Status Code: 404")) {
rethrow; rethrow;
@@ -100,7 +106,7 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
} }
if (stop) { if (stop) {
throw Exception("Image loading is stopped"); throw const _ImageLoadingStopException();
} }
if (data!.isEmpty) { if (data!.isEmpty) {
@@ -127,6 +133,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
} }
rethrow; rethrow;
} }
} on _ImageLoadingStopException {
rethrow;
} catch (e, s) { } catch (e, s) {
scheduleMicrotask(() { scheduleMicrotask(() {
PaintingBinding.instance.imageCache.evict(key); PaintingBinding.instance.imageCache.evict(key);
@@ -138,7 +146,10 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
} }
} }
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents); Future<Uint8List> load(
StreamController<ImageChunkEvent> chunkEvents,
void Function() checkStop,
);
String get key; String get key;
@@ -159,3 +170,7 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
} }
typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List); typedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);
class _ImageLoadingStopException implements Exception {
const _ImageLoadingStopException();
}

View File

@@ -1,4 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/network/images.dart'; import 'package:venera/network/images.dart';
@@ -26,9 +26,10 @@ class CachedImageProvider
static const _kMaxLoadingCount = 8; static const _kMaxLoadingCount = 8;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(chunkEvents, checkStop) async {
while(loadingCount > _kMaxLoadingCount) { while(loadingCount > _kMaxLoadingCount) {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
checkStop();
} }
loadingCount++; loadingCount++;
try { try {
@@ -37,6 +38,7 @@ class CachedImageProvider
return file.readAsBytes(); return file.readAsBytes();
} }
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
checkStop();
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes, cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes, expectedTotalBytes: progress.totalBytes,

View File

@@ -1,4 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
@@ -17,7 +17,7 @@ class HistoryImageProvider
final History history; final History history;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(chunkEvents, checkStop) async {
var url = history.cover; var url = history.cover;
if (!url.contains('/')) { if (!url.contains('/')) {
var localComic = LocalManager().find(history.id, history.type); var localComic = LocalManager().find(history.id, history.type);
@@ -27,6 +27,7 @@ class HistoryImageProvider
var comicSource = var comicSource =
history.type.comicSource ?? (throw "Comic source not found."); history.type.comicSource ?? (throw "Comic source not found.");
var comic = await comicSource.loadComicInfo!(history.id); var comic = await comicSource.loadComicInfo!(history.id);
checkStop();
url = comic.data.cover; url = comic.data.cover;
history.cover = url; history.cover = url;
HistoryManager().addHistory(history); HistoryManager().addHistory(history);
@@ -36,6 +37,7 @@ class HistoryImageProvider
history.type.sourceKey, history.type.sourceKey,
history.id, history.id,
)) { )) {
checkStop();
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes, cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes, expectedTotalBytes: progress.totalBytes,

View File

@@ -1,5 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future, StreamController;
import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -29,30 +28,36 @@ class ImageFavoritesProvider
String get eid => imageFavorite.eid; String get eid => imageFavorite.eid;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent>? chunkEvents) async { Future<Uint8List> load(
StreamController<ImageChunkEvent>? chunkEvents,
void Function()? checkStop,
) async {
var imageKey = imageFavorite.imageKey; var imageKey = imageFavorite.imageKey;
var localImage = await getImageFromLocal(); var localImage = await getImageFromLocal();
checkStop?.call();
if (localImage != null) { if (localImage != null) {
return localImage; return localImage;
} }
var cacheImage = await readFromCache(); var cacheImage = await readFromCache();
checkStop?.call();
if (cacheImage != null) { if (cacheImage != null) {
return cacheImage; return cacheImage;
} }
var gotImageKey = false; var gotImageKey = false;
if (imageKey == "") { if (imageKey == "") {
imageKey = await getImageKey(); imageKey = await getImageKey();
checkStop?.call();
gotImageKey = true; gotImageKey = true;
} }
Uint8List image; Uint8List image;
try { try {
image = await getImageFromNetwork(imageKey, chunkEvents); image = await getImageFromNetwork(imageKey, chunkEvents, checkStop);
} catch (e) { } catch (e) {
if (gotImageKey) { if (gotImageKey) {
rethrow; rethrow;
} else { } else {
imageKey = await getImageKey(); imageKey = await getImageKey();
image = await getImageFromNetwork(imageKey, chunkEvents); image = await getImageFromNetwork(imageKey, chunkEvents, checkStop);
} }
} }
await writeToCache(image); await writeToCache(image);
@@ -106,9 +111,13 @@ class ImageFavoritesProvider
} }
Future<Uint8List> getImageFromNetwork( Future<Uint8List> getImageFromNetwork(
String imageKey, StreamController<ImageChunkEvent>? chunkEvents) async { String imageKey,
StreamController<ImageChunkEvent>? chunkEvents,
void Function()? checkStop,
) async {
await for (var progress await for (var progress
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
checkStop?.call();
if (chunkEvents != null) { if (chunkEvents != null) {
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes, cumulativeBytesLoaded: progress.currentBytes,

View File

@@ -1,4 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
@@ -16,7 +16,7 @@ class LocalComicImageProvider
final LocalComic comic; final LocalComic comic;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(chunkEvents, checkStop) async {
File? file = comic.coverFile; File? file = comic.coverFile;
if(! await file.exists()) { if(! await file.exists()) {
file = null; file = null;
@@ -49,6 +49,7 @@ class LocalComicImageProvider
if(file == null) { if(file == null) {
throw "Error: Cover not found."; throw "Error: Cover not found.";
} }
checkStop();
var data = await file.readAsBytes(); var data = await file.readAsBytes();
if(data.isEmpty) { if(data.isEmpty) {
throw "Exception: Empty file(${file.path})."; throw "Exception: Empty file(${file.path}).";

View File

@@ -1,4 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
@@ -28,7 +28,7 @@ class LocalFavoriteImageProvider
} }
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(chunkEvents, checkStop) async {
var sourceKey = ComicSource.fromIntKey(intKey)?.key; var sourceKey = ComicSource.fromIntKey(intKey)?.key;
var fileName = key.hashCode.toString(); var fileName = key.hashCode.toString();
var file = File(FilePath.join(App.dataPath, 'favorite_cover', fileName)); var file = File(FilePath.join(App.dataPath, 'favorite_cover', fileName));
@@ -37,7 +37,9 @@ class LocalFavoriteImageProvider
} else { } else {
await file.create(recursive: true); await file.create(recursive: true);
} }
checkStop();
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey)) { await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey)) {
checkStop();
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes, cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes, expectedTotalBytes: progress.totalBytes,

View File

@@ -1,4 +1,4 @@
import 'dart:async' show Future, StreamController; import 'dart:async' show Future;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/flutter_qjs.dart';
@@ -12,7 +12,7 @@ import 'package:venera/foundation/appdata.dart';
class ReaderImageProvider class ReaderImageProvider
extends BaseImageProvider<image_provider.ReaderImageProvider> { extends BaseImageProvider<image_provider.ReaderImageProvider> {
/// Image provider for normal image. /// 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; final String imageKey;
@@ -22,8 +22,10 @@ class ReaderImageProvider
final String eid; final String eid;
final int page;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(chunkEvents, checkStop) async {
Uint8List? imageBytes; Uint8List? imageBytes;
if (imageKey.startsWith('file://')) { if (imageKey.startsWith('file://')) {
var file = File(imageKey); var file = File(imageKey);
@@ -35,6 +37,7 @@ class ReaderImageProvider
} else { } else {
await for (var event await for (var event
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
checkStop();
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: event.currentBytes, cumulativeBytesLoaded: event.currentBytes,
expectedTotalBytes: event.totalBytes, expectedTotalBytes: event.totalBytes,
@@ -60,14 +63,57 @@ class ReaderImageProvider
})() })()
'''); ''');
if (func is JSInvokable) { if (func is JSInvokable) {
var result = await func.invoke([imageBytes, cid, eid]); var result = func.invoke([imageBytes, cid, eid, page, sourceKey]);
func.free();
if (result is Uint8List) { 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();
} }
return imageBytes; }
func.free();
}
}
return imageBytes!;
} }
@override @override

View File

@@ -224,7 +224,7 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
onClick: () async { onClick: () async {
var temp = images[currentPage]; var temp = images[currentPage];
var imageProvider = ImageFavoritesProvider(temp); var imageProvider = ImageFavoritesProvider(temp);
var data = await imageProvider.load(null); var data = await imageProvider.load(null, null);
var fileType = detectFileType(data); var fileType = detectFileType(data);
var fileName = "${currentPage + 1}.${fileType.ext}"; var fileName = "${currentPage + 1}.${fileType.ext}";
await saveFile(filename: fileName, data: data); await saveFile(filename: fileName, data: data);

View File

@@ -673,6 +673,7 @@ ImageProvider _createImageProviderFromKey(
reader.type.comicSource?.key, reader.type.comicSource?.key,
reader.cid, reader.cid,
reader.eid, reader.eid,
reader.page,
); );
} }

View File

@@ -650,6 +650,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
reader.type.comicSource!.key, reader.type.comicSource!.key,
reader.cid, reader.cid,
reader.eid, reader.eid,
reader.page,
); );
} }
return InkWell( return InkWell(

View File

@@ -131,9 +131,10 @@ class _ReaderSettingsState extends State<ReaderSettings> {
"On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode"
.tl, .tl,
).toSliver(), ).toSliver(),
_PopupWindowSetting( _CallbackSetting(
title: "Custom Image Processing".tl, title: "Custom Image Processing".tl,
builder: () => _CustomImageProcessing(), callback: () => context.to(() => _CustomImageProcessing()),
actionTitle: "Edit".tl,
).toSliver(), ).toSliver(),
], ],
); );
@@ -163,10 +164,25 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> {
super.dispose(); super.dispose();
} }
int resetKey = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopUpWidgetScaffold( return Scaffold(
title: "Custom Image Processing".tl, 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( body: Column(
children: [ children: [
_SwitchSetting( _SwitchSetting(
@@ -182,6 +198,7 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> {
), ),
child: SizedBox.expand( child: SizedBox.expand(
child: CodeEditor( child: CodeEditor(
key: ValueKey(resetKey),
initialValue: appdata.settings['customImageProcessing'], initialValue: appdata.settings['customImageProcessing'],
onChanged: (value) { onChanged: (value) {
current = value; current = value;