mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Improve image loading
This commit is contained in:
@@ -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": "漫畫"
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
''';
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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}).";
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -673,6 +673,7 @@ ImageProvider _createImageProviderFromKey(
|
||||
reader.type.comicSource?.key,
|
||||
reader.cid,
|
||||
reader.eid,
|
||||
reader.page,
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -650,6 +650,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
reader.type.comicSource!.key,
|
||||
reader.cid,
|
||||
reader.eid,
|
||||
reader.page,
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user