mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
improve image api & update version code
This commit is contained in:
@@ -224,7 +224,25 @@ let Convert = {
|
||||
key: key,
|
||||
isEncode: false
|
||||
});
|
||||
}
|
||||
},
|
||||
/** Encode bytes to hex string
|
||||
* @param bytes {ArrayBuffer}
|
||||
* @return {string}
|
||||
*/
|
||||
hexEncode: (bytes) => {
|
||||
const hexDigits = '0123456789abcdef';
|
||||
const view = new Uint8Array(bytes);
|
||||
let charCodes = new Uint8Array(view.length * 2);
|
||||
let j = 0;
|
||||
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
let byte = view[i];
|
||||
charCodes[j++] = hexDigits.charCodeAt((byte >> 4) & 0xF);
|
||||
charCodes[j++] = hexDigits.charCodeAt(byte & 0xF);
|
||||
}
|
||||
|
||||
return String.fromCharCode(...charCodes);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1064,26 +1082,28 @@ class Image {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new image based on the current image without copying the data.
|
||||
* Modifying the new image will affect the current image.
|
||||
* fill [image] with range(srcX, srcY, width, height) to this image at (x, y)
|
||||
* @param x
|
||||
* @param y
|
||||
* @param image
|
||||
* @param srcX
|
||||
* @param srcY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns {Image|null}
|
||||
*/
|
||||
subImage(x, y, width, height) {
|
||||
let key = sendMessage({
|
||||
fillImageRangeAt(x, y, image, srcX, srcY, width, height) {
|
||||
sendMessage({
|
||||
method: "image",
|
||||
function: "subImage",
|
||||
function: "fillImageRangeAt",
|
||||
key: this.key,
|
||||
x: x,
|
||||
y: y,
|
||||
image: image.key,
|
||||
srcX: srcX,
|
||||
srcY: srcY,
|
||||
width: width,
|
||||
height: height
|
||||
})
|
||||
if(key == null) return null;
|
||||
return new Image(key);
|
||||
}
|
||||
|
||||
get width() {
|
||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.0.1";
|
||||
final version = "1.0.2";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
|
@@ -87,17 +87,16 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||
return await decode(buffer);
|
||||
} catch (e) {
|
||||
await CacheManager().delete(this.key);
|
||||
Object error = e;
|
||||
if (data.length < 2 * 1024) {
|
||||
// data is too short, it's likely that the data is text, not image
|
||||
try {
|
||||
var text = const Utf8Codec(allowMalformed: false).decoder.convert(data);
|
||||
error = Exception("Expected image data, but got text: $text");
|
||||
throw Exception("Expected image data, but got text: $text");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
rethrow;
|
||||
}
|
||||
} catch (e) {
|
||||
scheduleMicrotask(() {
|
||||
|
@@ -3,6 +3,7 @@ import 'dart:typed_data';
|
||||
import 'package:venera/foundation/cache_manager.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/consts.dart';
|
||||
import 'package:venera/utils/image.dart';
|
||||
|
||||
import 'app_dio.dart';
|
||||
|
||||
@@ -27,8 +28,8 @@ class ImageDownloader {
|
||||
configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {};
|
||||
}
|
||||
configs['headers'] ??= {};
|
||||
if(configs['headers']['user-agent'] == null
|
||||
&& configs['headers']['User-Agent'] == null) {
|
||||
if (configs['headers']['user-agent'] == null &&
|
||||
configs['headers']['User-Agent'] == null) {
|
||||
configs['headers']['user-agent'] = webUA;
|
||||
}
|
||||
|
||||
@@ -120,11 +121,22 @@ class ImageDownloader {
|
||||
buffer = configs['onResponse'](buffer);
|
||||
}
|
||||
|
||||
await CacheManager().writeCache(cacheKey, buffer);
|
||||
var data = Uint8List.fromList(buffer);
|
||||
buffer.clear();
|
||||
|
||||
if (configs['modifyImage'] != null) {
|
||||
var newData = await modifyImageWithScript(
|
||||
data,
|
||||
configs['modifyImage'],
|
||||
);
|
||||
data = newData;
|
||||
}
|
||||
|
||||
await CacheManager().writeCache(cacheKey, data);
|
||||
yield ImageDownloadProgress(
|
||||
currentBytes: buffer.length,
|
||||
totalBytes: buffer.length,
|
||||
imageBytes: Uint8List.fromList(buffer),
|
||||
currentBytes: data.length,
|
||||
totalBytes: data.length,
|
||||
imageBytes: data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -161,71 +161,76 @@ class _BodyState extends State<_Body> {
|
||||
for (var item in source.settings!.entries) {
|
||||
var key = item.key;
|
||||
String type = item.value['type'];
|
||||
if (type == "select") {
|
||||
var current = source.data['settings'][key];
|
||||
if (current == null) {
|
||||
var d = item.value['default'];
|
||||
for (var option in item.value['options']) {
|
||||
if (option['value'] == d) {
|
||||
current = option['text'] ?? option['value'];
|
||||
break;
|
||||
try {
|
||||
if (type == "select") {
|
||||
var current = source.data['settings'][key];
|
||||
if (current == null) {
|
||||
var d = item.value['default'];
|
||||
for (var option in item.value['options']) {
|
||||
if (option['value'] == d) {
|
||||
current = option['text'] ?? option['value'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
yield ListTile(
|
||||
title: Text((item.value['title'] as String).ts(source.key)),
|
||||
trailing: Select(
|
||||
current: (current as String).ts(source.key),
|
||||
values: (item.value['options'] as List)
|
||||
.map<String>(
|
||||
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
|
||||
.toList(),
|
||||
onTap: (i) {
|
||||
source.data['settings'][key] = item.value['options'][i]['value'];
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (type == "switch") {
|
||||
var current = source.data['settings'][key] ?? item.value['default'];
|
||||
yield ListTile(
|
||||
title: Text((item.value['title'] as String).ts(source.key)),
|
||||
trailing: Switch(
|
||||
value: current,
|
||||
onChanged: (v) {
|
||||
source.data['settings'][key] = v;
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (type == "input") {
|
||||
var current =
|
||||
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),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
showInputDialog(
|
||||
context: context,
|
||||
title: (item.value['title'] as String).ts(source.key),
|
||||
initialValue: current,
|
||||
inputValidator: item.value['validator'] == null
|
||||
? null
|
||||
: RegExp(item.value['validator']),
|
||||
onConfirm: (value) {
|
||||
source.data['settings'][key] = value;
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
yield ListTile(
|
||||
title: Text((item.value['title'] as String).ts(source.key)),
|
||||
trailing: Select(
|
||||
current: (current as String).ts(source.key),
|
||||
values: (item.value['options'] as List)
|
||||
.map<String>(
|
||||
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
|
||||
.toList(),
|
||||
onTap: (i) {
|
||||
source.data['settings'][key] = item.value['options'][i]['value'];
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (type == "switch") {
|
||||
var current = source.data['settings'][key] ?? item.value['default'];
|
||||
yield ListTile(
|
||||
title: Text((item.value['title'] as String).ts(source.key)),
|
||||
trailing: Switch(
|
||||
value: current,
|
||||
onChanged: (v) {
|
||||
source.data['settings'][key] = v;
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (type == "input") {
|
||||
var current =
|
||||
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),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
showInputDialog(
|
||||
context: context,
|
||||
title: (item.value['title'] as String).ts(source.key),
|
||||
initialValue: current,
|
||||
inputValidator: item.value['validator'] == null
|
||||
? null
|
||||
: RegExp(item.value['validator']),
|
||||
onConfirm: (value) {
|
||||
source.data['settings'][key] = value;
|
||||
source.saveData();
|
||||
setState(() {});
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
catch(e, s) {
|
||||
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -389,7 +389,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
|
||||
Widget buildPageInfoText() {
|
||||
var epName = context.reader.widget.chapters?.values
|
||||
.elementAt(context.reader.chapter - 1) ??
|
||||
.elementAtOrNull(context.reader.chapter - 1) ??
|
||||
"E${context.reader.chapter}";
|
||||
if (epName.length > 8) {
|
||||
epName = "${epName.substring(0, 8)}...";
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
@@ -12,7 +13,12 @@ class Image {
|
||||
|
||||
final int height;
|
||||
|
||||
Image(this._data, this.width, this.height);
|
||||
Image(this._data, this.width, this.height) {
|
||||
if (_data.length != width * height) {
|
||||
throw ArgumentError(
|
||||
'Invalid argument: data length must be equal to width * height.');
|
||||
}
|
||||
}
|
||||
|
||||
Image.empty(this.width, this.height) : _data = Uint32List(width * height);
|
||||
|
||||
@@ -25,7 +31,7 @@ class Image {
|
||||
throw Exception('Failed to decode image');
|
||||
}
|
||||
var image = Image(
|
||||
Uint32List.fromList(info.buffer.asUint32List()),
|
||||
info.buffer.asUint32List(),
|
||||
frame.image.width,
|
||||
frame.image.height,
|
||||
);
|
||||
@@ -34,6 +40,20 @@ class Image {
|
||||
}
|
||||
|
||||
Image copyRange(int x, int y, int width, int height) {
|
||||
if (width + x > this.width) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: x + width must be less than or equal to the image width.
|
||||
x: $x, width: $width, image width: ${this.width}
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
if (height + y > this.height) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: y + height must be less than or equal to the image height.
|
||||
y: $y, height: $height, image height: ${this.height}
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
var data = Uint32List(width * height);
|
||||
for (var j = 0; j < height; j++) {
|
||||
for (var i = 0; i < width; i++) {
|
||||
@@ -44,6 +64,20 @@ class Image {
|
||||
}
|
||||
|
||||
void fillImageAt(int x, int y, Image image) {
|
||||
if (x + image.width > width) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: x + image width must be less than or equal to the image width.
|
||||
x: $x, image width: ${image.width}, image width: $width
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
if (y + image.height > height) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: y + image height must be less than or equal to the image height.
|
||||
y: $y, image height: ${image.height}, image height: $height
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
for (var j = 0; j < image.height && (j + y) < height; j++) {
|
||||
for (var i = 0; i < image.width && (i + x) < width; i++) {
|
||||
_data[(j + y) * width + i + x] = image._data[j * image.width + i];
|
||||
@@ -51,6 +85,44 @@ class Image {
|
||||
}
|
||||
}
|
||||
|
||||
void fillImageRangeAt(
|
||||
int x, int y, Image image, int srcX, int srcY, int width, int height) {
|
||||
if (x + width > this.width) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: x + width must be less than or equal to the image width.
|
||||
x: $x, width: $width, image width: ${this.width}
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
if (y + height > this.height) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: y + height must be less than or equal to the image height.
|
||||
y: $y, height: $height, image height: ${this.height}
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
if (srcX + width > image.width) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: srcX + width must be less than or equal to the image width.
|
||||
srcX: $srcX, width: $width, image width: ${image.width}
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
if (srcY + height > image.height) {
|
||||
throw ArgumentError('''
|
||||
Invalid argument: srcY + height must be less than or equal to the image height.
|
||||
srcY: $srcY, height: $height, image height: ${image.height}
|
||||
'''
|
||||
.trim());
|
||||
}
|
||||
for (var j = 0; j < height; j++) {
|
||||
for (var i = 0; i < width; i++) {
|
||||
_data[(j + y) * this.width + i + x] =
|
||||
image._data[(j + srcY) * image.width + i + srcX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image copyAndRotate90() {
|
||||
var data = Uint32List(width * height);
|
||||
for (var j = 0; j < height; j++) {
|
||||
@@ -62,28 +134,37 @@ class Image {
|
||||
}
|
||||
|
||||
Color getPixel(int x, int y) {
|
||||
if (x < 0 || x >= width) {
|
||||
throw ArgumentError(
|
||||
'Invalid argument: x must be in the range of [0, $width).');
|
||||
}
|
||||
if (y < 0 || y >= height) {
|
||||
throw ArgumentError(
|
||||
'Invalid argument: y must be in the range of [0, $height).');
|
||||
}
|
||||
return Color.fromValue(_data[y * width + x]);
|
||||
}
|
||||
|
||||
void setPixel(int x, int y, Color color) {
|
||||
if (x < 0 || x >= width) {
|
||||
throw ArgumentError(
|
||||
'Invalid argument: x must be in the range of [0, $width).');
|
||||
}
|
||||
if (y < 0 || y >= height) {
|
||||
throw ArgumentError(
|
||||
'Invalid argument: y must be in the range of [0, $height).');
|
||||
}
|
||||
_data[y * width + x] = color.value;
|
||||
}
|
||||
|
||||
Image subImage(int x, int y, int width, int height) {
|
||||
var data = Uint32List.sublistView(
|
||||
_data,
|
||||
y * this.width + x,
|
||||
width * height,
|
||||
);
|
||||
return Image(data, width, height);
|
||||
}
|
||||
|
||||
Uint8List encodePng() {
|
||||
return lodepng.encodePng(lodepng.Image(
|
||||
var data = lodepng.encodePngToPointer(lodepng.Image(
|
||||
_data.buffer.asUint8List(),
|
||||
width,
|
||||
height,
|
||||
));
|
||||
return Pointer<Uint8>.fromAddress(data.address).asTypedList(data.length,
|
||||
finalizer: lodepng.ByteBuffer.finalizer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,16 +247,21 @@ class JsEngine {
|
||||
if (image2 == null) return null;
|
||||
image.fillImageAt(x, y, image2);
|
||||
return null;
|
||||
case 'subImage':
|
||||
case 'fillImageRangeAt':
|
||||
var key = message['key'];
|
||||
var image = images[key];
|
||||
if (image == null) return null;
|
||||
var x = message['x'];
|
||||
var y = message['y'];
|
||||
var key2 = message['image'];
|
||||
var image2 = images[key2];
|
||||
if (image2 == null) return null;
|
||||
var srcX = message['srcX'];
|
||||
var srcY = message['srcY'];
|
||||
var width = message['width'];
|
||||
var height = message['height'];
|
||||
var newImage = image.subImage(x, y, width, height);
|
||||
return setImage(newImage);
|
||||
image.fillImageRangeAt(x, y, image2, srcX, srcY, width, height);
|
||||
return null;
|
||||
case 'getWidth':
|
||||
var key = message['key'];
|
||||
var image = images[key];
|
||||
@@ -213,16 +299,18 @@ Future<Uint8List> modifyImageWithScript(Uint8List data, String script) async {
|
||||
jsEngine.runCode(script);
|
||||
var key = jsEngine.setImage(image);
|
||||
var res = jsEngine.runCode('''
|
||||
let image = new Image($key);
|
||||
let result = modifyImage(image);
|
||||
return result.key;
|
||||
''');
|
||||
let func = () => {
|
||||
let image = new Image($key);
|
||||
let result = modifyImage(image);
|
||||
return result.key;
|
||||
}
|
||||
func();
|
||||
''');
|
||||
var newImage = jsEngine.images[res];
|
||||
var data = newImage!.encodePng();
|
||||
return data;
|
||||
return Uint8List.fromList(data);
|
||||
});
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
_tasksCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -461,7 +461,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "115b896a04c270a8d6d5d7bea09dcd04047bfad3"
|
||||
resolved-ref: "5223cf4ce8aad1c2315db0093db3cc5c6c7191a8"
|
||||
url: "https://github.com/venera-app/lodepng_flutter"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
@@ -2,7 +2,7 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.1+101
|
||||
version: 1.0.2+102
|
||||
|
||||
environment:
|
||||
sdk: '>=3.5.0 <4.0.0'
|
||||
|
Reference in New Issue
Block a user