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": "启动时检查更新",
"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": "漫畫"
}
}

View File

@@ -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<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;
}
''';

View File

@@ -78,7 +78,13 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
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<T extends BaseImageProvider<T>>
}
if (stop) {
throw Exception("Image loading is stopped");
throw const _ImageLoadingStopException();
}
if (data!.isEmpty) {
@@ -127,6 +133,8 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
}
rethrow;
}
} on _ImageLoadingStopException {
rethrow;
} catch (e, s) {
scheduleMicrotask(() {
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;
@@ -159,3 +170,7 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
}
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/material.dart';
import 'package:venera/network/images.dart';
@@ -26,9 +26,10 @@ class CachedImageProvider
static const _kMaxLoadingCount = 8;
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Future<Uint8List> 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,

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/material.dart';
import 'package:venera/foundation/local.dart';
@@ -17,7 +17,7 @@ class HistoryImageProvider
final History history;
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Future<Uint8List> 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,

View File

@@ -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<Uint8List> load(StreamController<ImageChunkEvent>? chunkEvents) async {
Future<Uint8List> load(
StreamController<ImageChunkEvent>? 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<Uint8List> getImageFromNetwork(
String imageKey, StreamController<ImageChunkEvent>? chunkEvents) async {
String imageKey,
StreamController<ImageChunkEvent>? 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,

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/material.dart';
import 'package:venera/foundation/local.dart';
@@ -16,7 +16,7 @@ class LocalComicImageProvider
final LocalComic comic;
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Future<Uint8List> 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}).";

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/material.dart';
import 'package:venera/foundation/app.dart';
@@ -28,7 +28,7 @@ class LocalFavoriteImageProvider
}
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Future<Uint8List> 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,

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/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.ReaderImageProvider> {
/// 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<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Future<Uint8List> 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();
}
return imageBytes;
}
func.free();
}
}
return imageBytes!;
}
@override

View File

@@ -224,7 +224,7 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
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);

View File

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

View File

@@ -650,6 +650,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
reader.type.comicSource!.key,
reader.cid,
reader.eid,
reader.page,
);
}
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"
.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;