Merge pull request #17 from venera-app/dev

v1.0.2
This commit is contained in:
nyne
2024-11-05 22:55:32 +08:00
committed by GitHub
29 changed files with 905 additions and 157 deletions

View File

@@ -17,6 +17,13 @@ A comic reader that support reading local and network comics.
- View comments, tags, and other information of comics if the source supports - View comments, tags, and other information of comics if the source supports
- Login to comment, rate, and other operations if the source supports - Login to comment, rate, and other operations if the source supports
## Build from source
1. Clone the repository
2. Install flutter, see [flutter.dev](https://flutter.dev/docs/get-started/install)
3. Install rust, see [rustup.rs](https://rustup.rs/)
4. Build for your platform: e.g. `flutter build apk`
## Create a new comic source ## Create a new comic source
See [venera-configs](https://github.com/venera-app/venera-configs) See [venera-configs](https://github.com/venera-app/venera-configs)

View File

@@ -75,6 +75,9 @@ android {
buildTypes { buildTypes {
release { release {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
signingConfig signingConfigs.release signingConfig signingConfigs.release
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.outputs.all { output -> variant.outputs.all { output ->

View File

@@ -224,7 +224,25 @@ let Convert = {
key: key, key: key,
isEncode: false 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);
},
} }
/** /**
@@ -1000,3 +1018,117 @@ class ComicSource {
static sources = {} static sources = {}
} }
/// A reference to dart object.
/// The api can only be used in the comic.onImageLoad.modifyImage function
class Image {
key = 0;
constructor(key) {
this.key = key;
}
/**
* Copy the specified range of the image
* @param x
* @param y
* @param width
* @param height
* @returns {Image|null}
*/
copyRange(x, y, width, height) {
let key = sendMessage({
method: "image",
function: "copyRange",
key: this.key,
x: x,
y: y,
width: width,
height: height
})
if(key == null) return null;
return new Image(key);
}
/**
* Copy the image and rotate 90 degrees
* @returns {Image|null}
*/
copyAndRotate90() {
let key = sendMessage({
method: "image",
function: "copyAndRotate90",
key: this.key
})
if(key == null) return null;
return new Image(key);
}
/**
* fill [image] to this image at (x, y)
* @param x
* @param y
* @param image
*/
fillImageAt(x, y, image) {
sendMessage({
method: "image",
function: "fillImageAt",
key: this.key,
x: x,
y: y,
image: image.key
})
}
/**
* 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
*/
fillImageRangeAt(x, y, image, srcX, srcY, width, height) {
sendMessage({
method: "image",
function: "fillImageRangeAt",
key: this.key,
x: x,
y: y,
image: image.key,
srcX: srcX,
srcY: srcY,
width: width,
height: height
})
}
get width() {
return sendMessage({
method: "image",
function: "getWidth",
key: this.key
})
}
get height() {
return sendMessage({
method: "image",
function: "getHeight",
key: this.key
})
}
static empty(width, height) {
let key = sendMessage({
method: "image",
function: "emptyImage",
width: width,
height: height
})
return new Image(key);
}
}

View File

@@ -19,7 +19,7 @@
"Select": "选择", "Select": "选择",
"Imported @a comics": "已导入 @a 部漫画", "Imported @a comics": "已导入 @a 部漫画",
"Downloading": "下载中", "Downloading": "下载中",
"Back": "返回", "Back": "后退",
"Delete": "删除", "Delete": "删除",
"Full Screen": "全屏", "Full Screen": "全屏",
"Auto Page Turning": "自动翻页", "Auto Page Turning": "自动翻页",
@@ -155,7 +155,21 @@
"Start": "开始", "Start": "开始",
"Export App Data": "导出应用数据", "Export App Data": "导出应用数据",
"Import App Data": "导入应用数据", "Import App Data": "导入应用数据",
"Export": "导出" "Export": "导出",
"Download Threads": "下载线程数",
"Update Time": "更新时间",
"Copy ID": "复制ID",
"Copy URL": "复制URL",
"Create": "创建",
"Folder Name": "文件夹名称",
"Ranking": "排行",
"Download Selected": "下载选中",
"Download All": "下载全部",
"Order": "顺序",
"minAppVersion @version is required": "需要最低App版本 @version",
"Remove": "移除",
"Long press to zoom": "长按缩放",
"Updates Available": "更新可用"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -178,7 +192,7 @@
"Select": "選擇", "Select": "選擇",
"Imported @a comics": "已匯入 @a 部漫畫", "Imported @a comics": "已匯入 @a 部漫畫",
"Downloading": "下載中", "Downloading": "下載中",
"Back": "返回", "Back": "後退",
"Delete": "刪除", "Delete": "刪除",
"Full Screen": "全螢幕", "Full Screen": "全螢幕",
"Auto Page Turning": "自動翻頁", "Auto Page Turning": "自動翻頁",
@@ -313,6 +327,20 @@
"Start": "開始", "Start": "開始",
"Export App Data": "匯出應用數據", "Export App Data": "匯出應用數據",
"Import App Data": "匯入應用數據", "Import App Data": "匯入應用數據",
"Export": "匯出" "Export": "匯出",
"Download Threads": "下載線程數",
"Update Time": "更新時間",
"Copy ID": "複製ID",
"Copy URL": "複製URL",
"Create": "創建",
"Folder Name": "文件夾名稱",
"Ranking": "排行",
"Download Selected": "下載選中",
"Download All": "下載全部",
"Order": "順序",
"minAppVersion @version is required": "需要最低App版本 @version",
"Remove": "移除",
"Long press to zoom": "長按縮放",
"Updates Available": "更新可用"
} }
} }

View File

@@ -454,7 +454,9 @@ class _ComicDescription extends StatelessWidget {
), ),
).toAlign(Alignment.topCenter); ).toAlign(Alignment.topCenter);
}), }),
), )
else
const Spacer(),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [

View File

@@ -129,13 +129,14 @@ void showDialogMessage(BuildContext context, String title, String message) {
); );
} }
void showConfirmDialog({ Future<void> showConfirmDialog({
required BuildContext context, required BuildContext context,
required String title, required String title,
required String content, required String content,
required void Function() onConfirm, required void Function() onConfirm,
String confirmText = "Confirm",
}) { }) {
showDialog( return showDialog(
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => ContentDialog(
title: title, title: title,
@@ -146,7 +147,7 @@ void showConfirmDialog({
context.pop(); context.pop();
onConfirm(); onConfirm();
}, },
child: Text("Confirm".tl), child: Text(confirmText.tl),
), ),
], ],
), ),

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.0.1"; final version = "1.0.2";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -110,6 +110,9 @@ class _Settings with ChangeNotifier {
'enablePageAnimation': true, 'enablePageAnimation': true,
'language': 'system', // system, zh-CN, zh-TW, en-US 'language': 'system', // system, zh-CN, zh-TW, en-US
'cacheSize': 2048, // in MB 'cacheSize': 2048, // in MB
'downloadThreads': 5,
'enableLongPressToZoom': true,
'checkUpdateOnStart': true,
}; };
operator [](String key) { operator [](String key) {

View File

@@ -12,6 +12,7 @@ import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
import '../js_engine.dart'; import '../js_engine.dart';
import '../log.dart'; import '../log.dart';

View File

@@ -106,7 +106,9 @@ class ComicSourceParser {
if (minAppVersion != null) { if (minAppVersion != null) {
if (compareSemVer(minAppVersion, App.version.split('-').first)) { if (compareSemVer(minAppVersion, App.version.split('-').first)) {
throw ComicSourceParseException( throw ComicSourceParseException(
"minAppVersion $minAppVersion is required"); "minAppVersion @version is required"
.tlParams({"version": minAppVersion}),
);
} }
} }
for (var source in ComicSource.all()) { for (var source in ComicSource.all()) {
@@ -728,7 +730,7 @@ class ComicSourceParser {
return retryZone(func); return retryZone(func);
}; };
if(_checkExists("favorites.addFolder")) { if (_checkExists("favorites.addFolder")) {
addFolder = (name) async { addFolder = (name) async {
try { try {
await JsEngine().runCode(""" await JsEngine().runCode("""
@@ -741,7 +743,7 @@ class ComicSourceParser {
} }
}; };
} }
if(_checkExists("favorites.deleteFolder")) { if (_checkExists("favorites.deleteFolder")) {
deleteFolder = (key) async { deleteFolder = (key) async {
try { try {
await JsEngine().runCode(""" await JsEngine().runCode("""

View File

@@ -87,17 +87,16 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
return await decode(buffer); return await decode(buffer);
} catch (e) { } catch (e) {
await CacheManager().delete(this.key); await CacheManager().delete(this.key);
Object error = e;
if (data.length < 2 * 1024) { if (data.length < 2 * 1024) {
// data is too short, it's likely that the data is text, not image // data is too short, it's likely that the data is text, not image
try { try {
var text = const Utf8Codec(allowMalformed: false).decoder.convert(data); 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) { } catch (e) {
// ignore // ignore
} }
} }
throw error; rethrow;
} }
} catch (e) { } catch (e) {
scheduleMicrotask(() { scheduleMicrotask(() {

View File

@@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:html/parser.dart' as html; import 'package:html/parser.dart' as html;
@@ -238,7 +237,7 @@ mixin class _JSEngineApi {
Log.warning( Log.warning(
"JS Engine", "JS Engine",
"Too many documents, deleting the oldest: $shouldDelete\n" "Too many documents, deleting the oldest: $shouldDelete\n"
"Current documents: ${_documents.keys}", "Current documents: ${_documents.keys}",
); );
_documents.remove(shouldDelete); _documents.remove(shouldDelete);
} }
@@ -350,9 +349,6 @@ mixin class _JSEngineApi {
case "utf8": case "utf8":
return isEncode ? utf8.encode(value) : utf8.decode(value); return isEncode ? utf8.encode(value) : utf8.decode(value);
case "base64": case "base64":
if (value is String) {
value = utf8.encode(value);
}
return isEncode ? base64Encode(value) : base64Decode(value); return isEncode ? base64Encode(value) : base64Decode(value);
case "md5": case "md5":
return Uint8List.fromList(md5.convert(value).bytes); return Uint8List.fromList(md5.convert(value).bytes);
@@ -383,8 +379,21 @@ mixin class _JSEngineApi {
if (!isEncode) { if (!isEncode) {
var key = data["key"]; var key = data["key"];
var cipher = ECBBlockCipher(AESEngine()); var cipher = ECBBlockCipher(AESEngine());
cipher.init(false, KeyParameter(key)); cipher.init(
return cipher.process(value); false,
KeyParameter(key),
);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
} }
return null; return null;
case "aes-cbc": case "aes-cbc":
@@ -393,7 +402,17 @@ mixin class _JSEngineApi {
var iv = data["iv"]; var iv = data["iv"];
var cipher = CBCBlockCipher(AESEngine()); var cipher = CBCBlockCipher(AESEngine());
cipher.init(false, ParametersWithIV(KeyParameter(key), iv)); cipher.init(false, ParametersWithIV(KeyParameter(key), iv));
return cipher.process(value); var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
} }
return null; return null;
case "aes-cfb": case "aes-cfb":
@@ -402,7 +421,17 @@ mixin class _JSEngineApi {
var blockSize = data["blockSize"]; var blockSize = data["blockSize"];
var cipher = CFBBlockCipher(AESEngine(), blockSize); var cipher = CFBBlockCipher(AESEngine(), blockSize);
cipher.init(false, KeyParameter(key)); cipher.init(false, KeyParameter(key));
return cipher.process(value); var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
} }
return null; return null;
case "aes-ofb": case "aes-ofb":
@@ -411,7 +440,17 @@ mixin class _JSEngineApi {
var blockSize = data["blockSize"]; var blockSize = data["blockSize"];
var cipher = OFBBlockCipher(AESEngine(), blockSize); var cipher = OFBBlockCipher(AESEngine(), blockSize);
cipher.init(false, KeyParameter(key)); cipher.init(false, KeyParameter(key));
return cipher.process(value); var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
} }
return null; return null;
case "rsa": case "rsa":
@@ -426,8 +465,8 @@ mixin class _JSEngineApi {
default: default:
return value; return value;
} }
} catch (e) { } catch (e, s) {
Log.error("JS Engine", "Failed to convert $type: $e"); Log.error("JS Engine", "Failed to convert $type: $e", s);
return null; return null;
} }
} }

View File

@@ -3,8 +3,12 @@ import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:rhttp/rhttp.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/pages/comic_source_page.dart';
import 'package:venera/pages/main_page.dart'; import 'package:venera/pages/main_page.dart';
import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/app_links.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'components/components.dart'; import 'components/components.dart';
@@ -18,6 +22,7 @@ void main(List<String> args) {
return; return;
} }
runZonedGuarded(() async { runZonedGuarded(() async {
await Rhttp.init();
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await init(); await init();
if (App.isAndroid) { if (App.isAndroid) {
@@ -63,6 +68,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
@override @override
void initState() { void initState() {
checkUpdates();
App.registerForceRebuild(forceRebuild); App.registerForceRebuild(forceRebuild);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
super.initState(); super.initState();
@@ -163,6 +169,22 @@ class _MyAppState extends State<MyApp> {
}, },
); );
} }
void checkUpdates() async {
if(!appdata.settings['checkUpdateOnStart']) {
return;
}
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
var now = DateTime.now().millisecondsSinceEpoch;
if(now - lastCheck < 24 * 60 * 60 * 1000) {
return;
}
appdata.implicitData['lastCheckUpdate'] = now;
appdata.writeImplicitData();
await Future.delayed(const Duration(milliseconds: 300));
await checkUpdateUi(false);
await ComicSourcePage.checkComicSourceUpdate(true);
}
} }
class _SystemUiProvider extends StatelessWidget { class _SystemUiProvider extends StatelessWidget {

View File

@@ -2,8 +2,8 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:rhttp/rhttp.dart' as rhttp;
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/network/cache.dart'; import 'package:venera/network/cache.dart';
@@ -109,7 +109,7 @@ class AppDio with DioMixin {
AppDio([BaseOptions? options]) { AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions(); this.options = options ?? BaseOptions();
interceptors.add(MyLogInterceptor()); interceptors.add(MyLogInterceptor());
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient); httpClientAdapter = RHttpAdapter(const rhttp.ClientSettings());
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager()); interceptors.add(NetworkCacheManager());
interceptors.add(CloudflareInterceptor()); interceptors.add(CloudflareInterceptor());
@@ -136,8 +136,9 @@ class AppDio with DioMixin {
static String? proxy; static String? proxy;
static Future<String?> getProxy() async { static Future<String?> getProxy() async {
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") {
return null; return null;
}
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy']; if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
String res; String res;
@@ -187,10 +188,13 @@ class AppDio with DioMixin {
}) async { }) async {
proxy = await getProxy(); proxy = await getProxy();
if (_proxy != proxy) { if (_proxy != proxy) {
Log.info("Network", "Proxy changed to $proxy");
_proxy = proxy; _proxy = proxy;
(httpClientAdapter as IOHttpClientAdapter).close(); httpClientAdapter = RHttpAdapter(rhttp.ClientSettings(
httpClientAdapter = proxySettings: proxy == null
IOHttpClientAdapter(createHttpClient: createHttpClient); ? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!),
));
} }
Log.info( Log.info(
"Network", "Network",
@@ -209,3 +213,81 @@ class AppDio with DioMixin {
); );
} }
} }
class RHttpAdapter implements HttpClientAdapter {
rhttp.ClientSettings settings;
RHttpAdapter(this.settings) {
settings.copyWith(
redirectSettings: const rhttp.RedirectSettings.limited(5),
timeoutSettings: const rhttp.TimeoutSettings(
connectTimeout: Duration(seconds: 15),
keepAliveTimeout: Duration(seconds: 60),
keepAlivePing: Duration(seconds: 30),
),
httpVersionPref: rhttp.HttpVersionPref.http1_1,
);
}
@override
void close({bool force = false}) {}
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
var res = await rhttp.Rhttp.request(
method: switch (options.method) {
'GET' => rhttp.HttpMethod.get,
'POST' => rhttp.HttpMethod.post,
'PUT' => rhttp.HttpMethod.put,
'PATCH' => rhttp.HttpMethod.patch,
'DELETE' => rhttp.HttpMethod.delete,
'HEAD' => rhttp.HttpMethod.head,
'OPTIONS' => rhttp.HttpMethod.options,
'TRACE' => rhttp.HttpMethod.trace,
'CONNECT' => rhttp.HttpMethod.connect,
_ => throw ArgumentError('Unsupported method: ${options.method}'),
},
url: options.uri.toString(),
settings: settings,
expectBody: rhttp.HttpExpectBody.stream,
body: requestStream == null ? null : rhttp.HttpBody.stream(requestStream),
headers: rhttp.HttpHeaders.rawMap(
Map.fromEntries(
options.headers.entries.map(
(e) => MapEntry(e.key, e.value.toString().trim()),
),
),
),
);
if (res is! rhttp.HttpStreamResponse) {
throw Exception("Invalid response type: ${res.runtimeType}");
}
var headers = <String, List<String>>{};
for (var entry in res.headers) {
var key = entry.$1.toLowerCase();
headers[key] ??= [];
headers[key]!.add(entry.$2);
}
var data = res.body;
if(headers['content-encoding']?.contains('gzip') ?? false) {
// rhttp does not support gzip decoding
var buffer = <int>[];
await for (var chunk in data) {
buffer.addAll(chunk);
}
data = Stream.value(Uint8List.fromList(gzip.decode(buffer)));
buffer.clear();
}
return ResponseBody(
data,
res.statusCode,
statusMessage: null,
isRedirect: false,
headers: headers,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:flutter/widgets.dart' show ChangeNotifier;
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
@@ -155,7 +156,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
var tasks = <int, _ImageDownloadWrapper>{}; var tasks = <int, _ImageDownloadWrapper>{};
int get _maxConcurrentTasks => 5; int get _maxConcurrentTasks => (appdata.settings["downloadThreads"] as num).toInt();
void _scheduleTasks() { void _scheduleTasks() {
var images = _images![_images!.keys.elementAt(_chapter)]!; var images = _images![_images!.keys.elementAt(_chapter)]!;

View File

@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/consts.dart';
import 'package:venera/utils/image.dart';
import 'app_dio.dart'; import 'app_dio.dart';
@@ -27,8 +28,8 @@ class ImageDownloader {
configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {}; configs = comicSource?.getThumbnailLoadingConfig?.call(url) ?? {};
} }
configs['headers'] ??= {}; configs['headers'] ??= {};
if(configs['headers']['user-agent'] == null if (configs['headers']['user-agent'] == null &&
&& configs['headers']['User-Agent'] == null) { configs['headers']['User-Agent'] == null) {
configs['headers']['user-agent'] = webUA; configs['headers']['user-agent'] = webUA;
} }
@@ -120,11 +121,22 @@ class ImageDownloader {
buffer = configs['onResponse'](buffer); 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( yield ImageDownloadProgress(
currentBytes: buffer.length, currentBytes: data.length,
totalBytes: buffer.length, totalBytes: data.length,
imageBytes: Uint8List.fromList(buffer), imageBytes: data,
); );
} }
} }

View File

@@ -1576,7 +1576,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: selected.isEmpty ? null : () {
widget.finishSelect(selected); widget.finishSelect(selected);
context.pop(); context.pop();
}, },
@@ -1587,7 +1587,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
], ],
), ),
), ),
SizedBox(height: MediaQuery.of(context).padding.bottom + 4), SizedBox(height: MediaQuery.of(context).padding.bottom),
], ],
), ),
); );

View File

@@ -14,11 +14,11 @@ import 'package:venera/utils/translations.dart';
class ComicSourcePage extends StatefulWidget { class ComicSourcePage extends StatefulWidget {
const ComicSourcePage({super.key}); const ComicSourcePage({super.key});
static void checkComicSourceUpdate([bool showLoading = false]) async { static Future<void> checkComicSourceUpdate([bool implicit = false]) async {
if (ComicSource.all().isEmpty) { if (ComicSource.all().isEmpty) {
return; return;
} }
var controller = showLoading ? showLoadingDialog(App.rootContext) : null; var controller = implicit ? null : showLoadingDialog(App.rootContext);
var dio = AppDio(); var dio = AppDio();
var res = await dio.get<String>( var res = await dio.get<String>(
"https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json"); "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json");
@@ -40,7 +40,9 @@ class ComicSourcePage extends StatefulWidget {
} }
controller?.close(); controller?.close();
if (shouldUpdate.isEmpty) { if (shouldUpdate.isEmpty) {
App.rootContext.showMessage(message: "No Update Available".tl); if(!implicit) {
App.rootContext.showMessage(message: "No Update Available".tl);
}
return; return;
} }
var msg = ""; var msg = "";
@@ -48,10 +50,11 @@ class ComicSourcePage extends StatefulWidget {
msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n"; msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n";
} }
msg = msg.trim(); msg = msg.trim();
showConfirmDialog( await showConfirmDialog(
context: App.rootContext, context: App.rootContext,
title: "Updates Available".tl, title: "Updates Available".tl,
content: msg, content: msg,
confirmText: "Update",
onConfirm: () { onConfirm: () {
for (var key in shouldUpdate) { for (var key in shouldUpdate) {
var source = ComicSource.find(key); var source = ComicSource.find(key);
@@ -104,7 +107,7 @@ class _BodyState extends State<_Body> {
child: ListTile( child: ListTile(
leading: const Icon(Icons.update_outlined), leading: const Icon(Icons.update_outlined),
title: Text("Check updates".tl), title: Text("Check updates".tl),
onTap: () => ComicSourcePage.checkComicSourceUpdate(true), onTap: () => ComicSourcePage.checkComicSourceUpdate(false),
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
), ),
); );
@@ -161,71 +164,76 @@ class _BodyState extends State<_Body> {
for (var item in source.settings!.entries) { for (var item in source.settings!.entries) {
var key = item.key; var key = item.key;
String type = item.value['type']; String type = item.value['type'];
if (type == "select") { try {
var current = source.data['settings'][key]; if (type == "select") {
if (current == null) { var current = source.data['settings'][key];
var d = item.value['default']; if (current == null) {
for (var option in item.value['options']) { var d = item.value['default'];
if (option['value'] == d) { for (var option in item.value['options']) {
current = option['text'] ?? option['value']; if (option['value'] == d) {
break; 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)), catch(e, s) {
trailing: Select( Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
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;
},
);
},
),
);
} }
} }
} }
@@ -446,10 +454,11 @@ class _ComicSourceListState extends State<_ComicSourceList> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
var key = json![index]["key"]; var key = json![index]["key"];
var action = currentKey.contains(key) var action = currentKey.contains(key)
? const Icon(Icons.check) ? const Icon(Icons.check, size: 20).paddingRight(8)
: Tooltip( : Tooltip(
message: "Add", message: "Add",
child: IconButton( child: Button.icon(
color: context.colorScheme.primary,
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () async { onPressed: () async {
await widget.onAdd( await widget.onAdd(

View File

@@ -223,6 +223,9 @@ class _GalleryModeState extends State<_GalleryMode>
@override @override
void handleLongPressDown(Offset location) { void handleLongPressDown(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
var photoViewController = photoViewControllers[reader.page]!; var photoViewController = photoViewControllers[reader.page]!;
double target = photoViewController.getInitialScale!.call()! * 1.75; double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = MediaQuery.of(context).size; var size = MediaQuery.of(context).size;
@@ -234,6 +237,9 @@ class _GalleryModeState extends State<_GalleryMode>
@override @override
void handleLongPressUp(Offset location) { void handleLongPressUp(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
var photoViewController = photoViewControllers[reader.page]!; var photoViewController = photoViewControllers[reader.page]!;
double target = photoViewController.getInitialScale!.call()!; double target = photoViewController.getInitialScale!.call()!;
photoViewController.animateScale?.call(target); photoViewController.animateScale?.call(target);
@@ -509,6 +515,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
@override @override
void handleLongPressDown(Offset location) { void handleLongPressDown(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
double target = photoViewController.getInitialScale!.call()! * 1.75; double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = MediaQuery.of(context).size; var size = MediaQuery.of(context).size;
photoViewController.animateScale?.call( photoViewController.animateScale?.call(
@@ -519,6 +528,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
@override @override
void handleLongPressUp(Offset location) { void handleLongPressUp(Offset location) {
if(!appdata.settings['enableLongPressToZoom']) {
return;
}
double target = photoViewController.getInitialScale!.call()!; double target = photoViewController.getInitialScale!.call()!;
photoViewController.animateScale?.call(target); photoViewController.animateScale?.call(target);
} }

View File

@@ -393,7 +393,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
Widget buildPageInfoText() { Widget buildPageInfoText() {
var epName = context.reader.widget.chapters?.values var epName = context.reader.widget.chapters?.values
.elementAt(context.reader.chapter - 1) ?? .elementAtOrNull(context.reader.chapter - 1) ??
"E${context.reader.chapter}"; "E${context.reader.chapter}";
if (epName.length > 8) { if (epName.length > 8) {
epName = "${epName.substring(0, 8)}..."; epName = "${epName.substring(0, 8)}...";

View File

@@ -53,30 +53,7 @@ class _AboutSettingsState extends State<AboutSettings> {
setState(() { setState(() {
isCheckingUpdate = true; isCheckingUpdate = true;
}); });
checkUpdate().then((value) { checkUpdateUi().then((value) {
if (value) {
showDialog(
context: App.rootContext,
builder: (context) {
return ContentDialog(
title: "New version available".tl,
content: Text(
"A new version is available. Do you want to update now?"
.tl),
actions: [
Button.text(
onPressed: () {
Navigator.pop(context);
launchUrlString(
"https://github.com/venera-app/venera/releases");
},
child: Text("Update".tl),
),
]);
});
} else {
context.showMessage(message: "No new version available".tl);
}
setState(() { setState(() {
isCheckingUpdate = false; isCheckingUpdate = false;
}); });
@@ -108,6 +85,33 @@ Future<bool> checkUpdate() async {
return false; return false;
} }
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
var value = await checkUpdate();
if (value) {
showDialog(
context: App.rootContext,
builder: (context) {
return ContentDialog(
title: "New version available".tl,
content: Text(
"A new version is available. Do you want to update now?".tl),
actions: [
Button.text(
onPressed: () {
Navigator.pop(context);
launchUrlString(
"https://github.com/venera-app/venera/releases");
},
child: Text("Update".tl),
),
],
);
});
} else if (showMessageIfNoUpdate) {
App.rootContext.showMessage(message: "No new version available".tl);
}
}
/// return true if version1 > version2 /// return true if version1 > version2
bool _compareVersion(String version1, String version2) { bool _compareVersion(String version1, String version2) {
var v1 = version1.split("."); var v1 = version1.split(".");

View File

@@ -17,6 +17,13 @@ class _NetworkSettingsState extends State<NetworkSettings> {
title: "Proxy".tl, title: "Proxy".tl,
builder: () => const _ProxySettingView(), builder: () => const _ProxySettingView(),
).toSliver(), ).toSliver(),
_SliderSetting(
title: "Download Threads".tl,
settingsIndex: 'downloadThreads',
interval: 1,
min: 1,
max: 16,
).toSliver(),
], ],
); );
} }
@@ -42,50 +49,50 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
// USERNAME:PASSWORD@HOST:PORT // USERNAME:PASSWORD@HOST:PORT
String toProxyStr() { String toProxyStr() {
if(type == 'direct') { if (type == 'direct') {
return 'direct'; return 'direct';
} else if(type == 'system') { } else if (type == 'system') {
return 'system'; return 'system';
} }
var res = ''; var res = '';
if(username.isNotEmpty) { if (username.isNotEmpty) {
res += username; res += username;
if(password.isNotEmpty) { if (password.isNotEmpty) {
res += ':$password'; res += ':$password';
} }
res += '@'; res += '@';
} }
res += host; res += host;
if(port.isNotEmpty) { if (port.isNotEmpty) {
res += ':$port'; res += ':$port';
} }
return res; return res;
} }
void parseProxyString(String proxy) { void parseProxyString(String proxy) {
if(proxy == 'direct') { if (proxy == 'direct') {
type = 'direct'; type = 'direct';
return; return;
} else if(proxy == 'system') { } else if (proxy == 'system') {
type = 'system'; type = 'system';
return; return;
} }
type = 'manual'; type = 'manual';
var parts = proxy.split('@'); var parts = proxy.split('@');
if(parts.length == 2) { if (parts.length == 2) {
var auth = parts[0].split(':'); var auth = parts[0].split(':');
if(auth.length == 2) { if (auth.length == 2) {
username = auth[0]; username = auth[0];
password = auth[1]; password = auth[1];
} }
parts = parts[1].split(':'); parts = parts[1].split(':');
if(parts.length == 2) { if (parts.length == 2) {
host = parts[0]; host = parts[0];
port = parts[1]; port = parts[1];
} }
} else { } else {
parts = proxy.split(':'); parts = proxy.split(':');
if(parts.length == 2) { if (parts.length == 2) {
host = parts[0]; host = parts[0];
port = parts[1]; port = parts[1];
} }
@@ -140,7 +147,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
}); });
}, },
), ),
if(type == 'manual') buildManualProxy(), if (type == 'manual') buildManualProxy(),
], ],
), ),
), ),
@@ -164,7 +171,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
host = v; host = v;
}, },
validator: (v) { validator: (v) {
if(v?.isEmpty ?? false) { if (v?.isEmpty ?? false) {
return "Host cannot be empty".tl; return "Host cannot be empty".tl;
} }
return null; return null;
@@ -181,10 +188,10 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
port = v; port = v;
}, },
validator: (v) { validator: (v) {
if(v?.isEmpty ?? true) { if (v?.isEmpty ?? true) {
return null; return null;
} }
if(int.tryParse(v!) == null) { if (int.tryParse(v!) == null) {
return "Port must be a number".tl; return "Port must be a number".tl;
} }
return null; return null;
@@ -201,7 +208,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
username = v; username = v;
}, },
validator: (v) { validator: (v) {
if((v?.isEmpty ?? false) && password.isNotEmpty) { if ((v?.isEmpty ?? false) && password.isNotEmpty) {
return "Username cannot be empty".tl; return "Username cannot be empty".tl;
} }
return null; return null;
@@ -221,7 +228,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
const SizedBox(height: 16), const SizedBox(height: 16),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
if(formKey.currentState?.validate() ?? false) { if (formKey.currentState?.validate() ?? false) {
appdata.settings['proxy'] = toProxyStr(); appdata.settings['proxy'] = toProxyStr();
appdata.saveData(); appdata.saveData();
App.rootContext.pop(); App.rootContext.pop();

View File

@@ -54,6 +54,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("autoPageTurningInterval"); widget.onChanged?.call("autoPageTurningInterval");
}, },
).toSliver(), ).toSliver(),
_SwitchSetting(
title: 'Long press to zoom'.tl,
settingKey: 'enableLongPressToZoom',
onChanged: () {
widget.onChanged?.call('enableLongPressToZoom');
},
).toSliver(),
], ],
); );
} }

316
lib/utils/image.dart Normal file
View File

@@ -0,0 +1,316 @@
import 'dart:ffi';
import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:lodepng_flutter/lodepng_flutter.dart' as lodepng;
class Image {
final Uint32List _data;
final int width;
final int 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);
static Future<Image> decodeImage(Uint8List data) async {
var codec = await ui.instantiateImageCodec(data);
var frame = await codec.getNextFrame();
codec.dispose();
var info = await frame.image.toByteData();
if (info == null) {
throw Exception('Failed to decode image');
}
var image = Image(
info.buffer.asUint32List(),
frame.image.width,
frame.image.height,
);
frame.image.dispose();
return 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++) {
data[j * width + i] = _data[(j + y) * this.width + i + x];
}
}
return Image(data, width, height);
}
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];
}
}
}
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++) {
for (var i = 0; i < width; i++) {
data[i * height + height - j - 1] = _data[j * width + i];
}
}
return Image(data, height, width);
}
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;
}
Uint8List encodePng() {
var data = lodepng.encodePngToPointer(lodepng.Image(
_data.buffer.asUint8List(),
width,
height,
));
return Pointer<Uint8>.fromAddress(data.address).asTypedList(data.length,
finalizer: lodepng.ByteBuffer.finalizer);
}
}
class Color {
final int value;
Color(int r, int g, int b, [int a = 255])
: value = (a << 24) | (r << 16) | (g << 8) | b;
Color.fromValue(this.value);
int get r => (value >> 16) & 0xFF;
int get g => (value >> 8) & 0xFF;
int get b => value & 0xFF;
int get a => (value >> 24) & 0xFF;
}
class JsEngine {
static final JsEngine _instance = JsEngine._();
factory JsEngine() => _instance;
JsEngine._() {
_engine = FlutterQjs();
_engine!.dispatch();
var setGlobalFunc =
_engine!.evaluate("(key, value) => { this[key] = value; }");
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
setGlobalFunc.free();
}
FlutterQjs? _engine;
dynamic runCode(String js, [String? name]) {
return _engine!.evaluate(js, name: name);
}
var images = <int, Image>{};
int _key = 0;
int setImage(Image image) {
var key = _key++;
images[key] = image;
return key;
}
Object? _messageReceiver(dynamic message) {
if (message is! Map) return null;
var method = message['method'];
if (method == 'image') {
switch (message['function']) {
case 'copyRange':
var key = message['key'];
var image = images[key];
if (image == null) return null;
var x = message['x'];
var y = message['y'];
var width = message['width'];
var height = message['height'];
var newImage = image.copyRange(x, y, width, height);
return setImage(newImage);
case 'copyAndRotate90':
var key = message['key'];
var image = images[key];
if (image == null) return null;
var newImage = image.copyAndRotate90();
return setImage(newImage);
case 'fillImageAt':
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;
image.fillImageAt(x, y, image2);
return null;
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'];
image.fillImageRangeAt(x, y, image2, srcX, srcY, width, height);
return null;
case 'getWidth':
var key = message['key'];
var image = images[key];
if (image == null) return null;
return image.width;
case 'getHeight':
var key = message['key'];
var image = images[key];
if (image == null) return null;
return image.height;
case 'emptyImage':
var width = message['width'];
var height = message['height'];
var newImage = Image.empty(width, height);
return setImage(newImage);
}
}
return null;
}
}
var _tasksCount = 0;
Future<Uint8List> modifyImageWithScript(Uint8List data, String script) async {
while (_tasksCount > 3) {
await Future.delayed(const Duration(milliseconds: 200));
}
_tasksCount++;
try {
var image = await Image.decodeImage(data);
var initJs = await rootBundle.loadString('assets/init.js');
return await Isolate.run(() {
var jsEngine = JsEngine();
jsEngine.runCode(initJs, '<init>');
jsEngine.runCode(script);
var key = jsEngine.setImage(image);
var res = jsEngine.runCode('''
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 Uint8List.fromList(data);
});
} finally {
_tasksCount--;
}
}

View File

@@ -14,6 +14,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
lodepng_flutter
rhttp
zip_flutter zip_flutter
) )

View File

@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
url: "https://pub.dev"
source: hosted
version: "2.1.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -341,6 +349,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.1" version: "5.0.1"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "5fe868d3cb8cbc4d83091748552e03f00ccfa41b8e44691bc382611f831d5f8b"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -368,6 +384,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
gtk: gtk:
dependency: transitive dependency: transitive
description: description:
@@ -424,6 +448,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.1" version: "0.7.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -456,6 +488,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
lodepng_flutter:
dependency: "direct main"
description:
path: "."
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
resolved-ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
url: "https://github.com/venera-app/lodepng_flutter"
source: git
version: "0.0.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -585,6 +626,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.1" version: "3.9.1"
rhttp:
dependency: "direct main"
description:
name: rhttp
sha256: "92fb57dea6338370efe1e4e2101e8b521f91f15bc60ef6908469b4392dd9803a"
url: "https://pub.dev"
source: hosted
version: "0.9.1"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -849,5 +898,5 @@ packages:
source: git source: git
version: "0.0.1" version: "0.0.1"
sdks: sdks:
dart: ">=3.5.0 <4.0.0" dart: ">=3.5.4 <4.0.0"
flutter: ">=3.24.4" flutter: ">=3.24.4"

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.0.1+101 version: 1.0.2+102
environment: environment:
sdk: '>=3.5.0 <4.0.0' sdk: '>=3.5.0 <4.0.0'
@@ -54,6 +54,11 @@ dependencies:
zip_flutter: zip_flutter:
git: git:
url: https://github.com/wgh136/zip_flutter url: https://github.com/wgh136/zip_flutter
lodepng_flutter:
git:
url: https://github.com/venera-app/lodepng_flutter
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
rhttp: 0.9.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -54,6 +54,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugi
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files

View File

@@ -16,6 +16,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
lodepng_flutter
rhttp
zip_flutter zip_flutter
) )