From 45e7f0dfc218b37bb694f16ae0ef964a3ede00bb Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 3 Nov 2024 15:49:34 +0800 Subject: [PATCH 01/12] add download threads setting --- assets/translation.json | 6 +++-- lib/foundation/appdata.dart | 1 + lib/network/download.dart | 3 ++- lib/pages/settings/network.dart | 41 +++++++++++++++++++-------------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 796bab9..d7a25c1 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -155,7 +155,8 @@ "Start": "开始", "Export App Data": "导出应用数据", "Import App Data": "导入应用数据", - "Export": "导出" + "Export": "导出", + "Download Threads": "下载线程数" }, "zh_TW": { "Home": "首頁", @@ -313,6 +314,7 @@ "Start": "開始", "Export App Data": "匯出應用數據", "Import App Data": "匯入應用數據", - "Export": "匯出" + "Export": "匯出", + "Download Threads": "下載線程數" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index a81f42e..e071ef7 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -110,6 +110,7 @@ class _Settings with ChangeNotifier { 'enablePageAnimation': true, 'language': 'system', // system, zh-CN, zh-TW, en-US 'cacheSize': 2048, // in MB + 'downloadThreads': 5, }; operator [](String key) { diff --git a/lib/network/download.dart b/lib/network/download.dart index 052a8c0..f0c3748 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -1,6 +1,7 @@ import 'dart:async'; 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_type.dart'; import 'package:venera/foundation/local.dart'; @@ -155,7 +156,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { var tasks = {}; - int get _maxConcurrentTasks => 5; + int get _maxConcurrentTasks => (appdata.settings["downloadThreads"] as num).toInt(); void _scheduleTasks() { var images = _images![_images!.keys.elementAt(_chapter)]!; diff --git a/lib/pages/settings/network.dart b/lib/pages/settings/network.dart index 0cc2545..21042ea 100644 --- a/lib/pages/settings/network.dart +++ b/lib/pages/settings/network.dart @@ -17,6 +17,13 @@ class _NetworkSettingsState extends State { title: "Proxy".tl, builder: () => const _ProxySettingView(), ).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 String toProxyStr() { - if(type == 'direct') { + if (type == 'direct') { return 'direct'; - } else if(type == 'system') { + } else if (type == 'system') { return 'system'; } var res = ''; - if(username.isNotEmpty) { + if (username.isNotEmpty) { res += username; - if(password.isNotEmpty) { + if (password.isNotEmpty) { res += ':$password'; } res += '@'; } res += host; - if(port.isNotEmpty) { + if (port.isNotEmpty) { res += ':$port'; } return res; } void parseProxyString(String proxy) { - if(proxy == 'direct') { + if (proxy == 'direct') { type = 'direct'; return; - } else if(proxy == 'system') { + } else if (proxy == 'system') { type = 'system'; return; } type = 'manual'; var parts = proxy.split('@'); - if(parts.length == 2) { + if (parts.length == 2) { var auth = parts[0].split(':'); - if(auth.length == 2) { + if (auth.length == 2) { username = auth[0]; password = auth[1]; } parts = parts[1].split(':'); - if(parts.length == 2) { + if (parts.length == 2) { host = parts[0]; port = parts[1]; } } else { parts = proxy.split(':'); - if(parts.length == 2) { + if (parts.length == 2) { host = parts[0]; 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; }, validator: (v) { - if(v?.isEmpty ?? false) { + if (v?.isEmpty ?? false) { return "Host cannot be empty".tl; } return null; @@ -181,10 +188,10 @@ class _ProxySettingViewState extends State<_ProxySettingView> { port = v; }, validator: (v) { - if(v?.isEmpty ?? true) { + if (v?.isEmpty ?? true) { return null; } - if(int.tryParse(v!) == null) { + if (int.tryParse(v!) == null) { return "Port must be a number".tl; } return null; @@ -201,7 +208,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> { username = v; }, validator: (v) { - if((v?.isEmpty ?? false) && password.isNotEmpty) { + if ((v?.isEmpty ?? false) && password.isNotEmpty) { return "Username cannot be empty".tl; } return null; @@ -221,7 +228,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> { const SizedBox(height: 16), FilledButton( onPressed: () { - if(formKey.currentState?.validate() ?? false) { + if (formKey.currentState?.validate() ?? false) { appdata.settings['proxy'] = toProxyStr(); appdata.saveData(); App.rootContext.pop(); From 0fbe9677b94f657bef625b2f0c42a165e0c9c841 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 4 Nov 2024 12:28:58 +0800 Subject: [PATCH 02/12] image api --- assets/init.js | 114 +++++++++++- lib/utils/image.dart | 228 ++++++++++++++++++++++++ linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 11 +- pubspec.yaml | 3 + windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 lib/utils/image.dart diff --git a/assets/init.js b/assets/init.js index 9f00eb3..b9d68cd 100644 --- a/assets/init.js +++ b/assets/init.js @@ -999,4 +999,116 @@ class ComicSource { init() { } static sources = {} -} \ No newline at end of file +} + +/// 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 + }) + } + + /** + * Create a new image based on the current image without copying the data. + * Modifying the new image will affect the current image. + * @param x + * @param y + * @param width + * @param height + * @returns {Image|null} + */ + subImage(x, y, width, height) { + let key = sendMessage({ + method: "image", + function: "subImage", + key: this.key, + x: x, + y: y, + width: width, + height: height + }) + if(key == null) return null; + return new Image(key); + } + + 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); + } +} diff --git a/lib/utils/image.dart b/lib/utils/image.dart new file mode 100644 index 0000000..d8b1411 --- /dev/null +++ b/lib/utils/image.dart @@ -0,0 +1,228 @@ +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); + + Image.empty(this.width, this.height) : _data = Uint32List(width * height); + + static Future 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( + Uint32List.fromList(info.buffer.asUint32List()), + frame.image.width, + frame.image.height, + ); + frame.image.dispose(); + return image; + } + + Image copyRange(int x, int y, int width, int height) { + 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) { + 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]; + } + } + } + + 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) { + return Color.fromValue(_data[y * width + x]); + } + + void setPixel(int x, int y, Color color) { + _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( + _data.buffer.asUint8List(), + width, + height, + )); + } +} + +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 _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 'subImage': + 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.subImage(x, y, width, height); + return setImage(newImage); + 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 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, ''); + jsEngine.runCode(script); + var key = jsEngine.setImage(image); + var res = jsEngine.runCode(''' + let image = new Image($key); + let result = modifyImage(image); + return result.key; + '''); + var newImage = jsEngine.images[res]; + var data = newImage!.encodePng(); + return data; + }); + } + finally { + _tasksCount--; + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index c0fb5f6..6558394 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + lodepng_flutter zip_flutter ) diff --git a/pubspec.lock b/pubspec.lock index 623cf1d..8ef4e04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,6 +456,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + lodepng_flutter: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "115b896a04c270a8d6d5d7bea09dcd04047bfad3" + url: "https://github.com/venera-app/lodepng_flutter" + source: git + version: "0.0.1" matcher: dependency: transitive description: @@ -849,5 +858,5 @@ packages: source: git version: "0.0.1" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.5.4 <4.0.0" flutter: ">=3.24.4" diff --git a/pubspec.yaml b/pubspec.yaml index fb39da7..6bbdd3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,9 @@ dependencies: zip_flutter: git: url: https://github.com/wgh136/zip_flutter + lodepng_flutter: + git: + url: https://github.com/venera-app/lodepng_flutter dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 693c576..ff62697 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + lodepng_flutter zip_flutter ) From 07f8f2a4af265b90b3a6c0ceb881694de621513c Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 4 Nov 2024 17:47:58 +0800 Subject: [PATCH 03/12] fix aes decryption --- lib/foundation/js_engine.dart | 63 ++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 2eefa60..8ae647f 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; - import 'package:crypto/crypto.dart'; import 'package:flutter/services.dart'; import 'package:html/parser.dart' as html; @@ -238,7 +237,7 @@ mixin class _JSEngineApi { Log.warning( "JS Engine", "Too many documents, deleting the oldest: $shouldDelete\n" - "Current documents: ${_documents.keys}", + "Current documents: ${_documents.keys}", ); _documents.remove(shouldDelete); } @@ -350,9 +349,6 @@ mixin class _JSEngineApi { case "utf8": return isEncode ? utf8.encode(value) : utf8.decode(value); case "base64": - if (value is String) { - value = utf8.encode(value); - } return isEncode ? base64Encode(value) : base64Decode(value); case "md5": return Uint8List.fromList(md5.convert(value).bytes); @@ -383,8 +379,21 @@ mixin class _JSEngineApi { if (!isEncode) { var key = data["key"]; var cipher = ECBBlockCipher(AESEngine()); - cipher.init(false, KeyParameter(key)); - return cipher.process(value); + cipher.init( + 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; case "aes-cbc": @@ -393,7 +402,17 @@ mixin class _JSEngineApi { var iv = data["iv"]; var cipher = CBCBlockCipher(AESEngine()); 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; case "aes-cfb": @@ -402,7 +421,17 @@ mixin class _JSEngineApi { var blockSize = data["blockSize"]; var cipher = CFBBlockCipher(AESEngine(), blockSize); 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; case "aes-ofb": @@ -411,7 +440,17 @@ mixin class _JSEngineApi { var blockSize = data["blockSize"]; var cipher = OFBBlockCipher(AESEngine(), blockSize); 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; case "rsa": @@ -426,8 +465,8 @@ mixin class _JSEngineApi { default: return value; } - } catch (e) { - Log.error("JS Engine", "Failed to convert $type: $e"); + } catch (e, s) { + Log.error("JS Engine", "Failed to convert $type: $e", s); return null; } } From b49e528ff44b1d8d8c55cdffe700304a7063563e Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 13:13:32 +0800 Subject: [PATCH 04/12] improve image api & update version code --- assets/init.js | 38 +++-- lib/foundation/app.dart | 2 +- .../image_provider/base_image_provider.dart | 5 +- lib/network/images.dart | 24 +++- lib/pages/comic_source_page.dart | 129 +++++++++-------- lib/pages/reader/scaffold.dart | 2 +- lib/utils/image.dart | 134 +++++++++++++++--- pubspec.lock | 2 +- pubspec.yaml | 2 +- 9 files changed, 231 insertions(+), 107 deletions(-) diff --git a/assets/init.js b/assets/init.js index b9d68cd..932590c 100644 --- a/assets/init.js +++ b/assets/init.js @@ -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() { diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 056cfc7..e1456b6 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -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; diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index cdc1797..57dd120 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -87,17 +87,16 @@ abstract class BaseImageProvider> 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(() { diff --git a/lib/network/images.dart b/lib/network/images.dart index ea887bd..29c9005 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -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, ); } } diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 5710473..d6ff26e 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -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( + (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( - (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"); } } } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 537f2b0..c48c3b7 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -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)}..."; diff --git a/lib/utils/image.dart b/lib/utils/image.dart index d8b1411..b7488b8 100644 --- a/lib/utils/image.dart +++ b/lib/utils/image.dart @@ -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.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 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--; } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 8ef4e04..5f59f6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 6bbdd3b..c77fc04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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' From adb6cdd0c1683bb657e12b5fd0da4d3e2270fe4a Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 15:27:46 +0800 Subject: [PATCH 05/12] improve ui --- assets/translation.json | 30 ++++++++++++++++--- lib/components/comic.dart | 4 ++- lib/foundation/comic_source/comic_source.dart | 1 + lib/foundation/comic_source/parser.dart | 8 +++-- lib/pages/comic_page.dart | 4 +-- lib/pages/comic_source_page.dart | 3 +- 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index d7a25c1..d8e93c8 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -19,7 +19,7 @@ "Select": "选择", "Imported @a comics": "已导入 @a 部漫画", "Downloading": "下载中", - "Back": "返回", + "Back": "后退", "Delete": "删除", "Full Screen": "全屏", "Auto Page Turning": "自动翻页", @@ -156,7 +156,18 @@ "Export App Data": "导出应用数据", "Import App Data": "导入应用数据", "Export": "导出", - "Download Threads": "下载线程数" + "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": "移除" }, "zh_TW": { "Home": "首頁", @@ -179,7 +190,7 @@ "Select": "選擇", "Imported @a comics": "已匯入 @a 部漫畫", "Downloading": "下載中", - "Back": "返回", + "Back": "後退", "Delete": "刪除", "Full Screen": "全螢幕", "Auto Page Turning": "自動翻頁", @@ -315,6 +326,17 @@ "Export App Data": "匯出應用數據", "Import App Data": "匯入應用數據", "Export": "匯出", - "Download Threads": "下載線程數" + "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": "移除" } } \ No newline at end of file diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 2c4c4c3..ab8c073 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -454,7 +454,9 @@ class _ComicDescription extends StatelessWidget { ), ).toAlign(Alignment.topCenter); }), - ), + ) + else + const Spacer(), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index 06621ad..f4c64df 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -12,6 +12,7 @@ import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; +import 'package:venera/utils/translations.dart'; import '../js_engine.dart'; import '../log.dart'; diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index ed723df..b02c54f 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -106,7 +106,9 @@ class ComicSourceParser { if (minAppVersion != null) { if (compareSemVer(minAppVersion, App.version.split('-').first)) { throw ComicSourceParseException( - "minAppVersion $minAppVersion is required"); + "minAppVersion @version is required" + .tlParams({"version": minAppVersion}), + ); } } for (var source in ComicSource.all()) { @@ -728,7 +730,7 @@ class ComicSourceParser { return retryZone(func); }; - if(_checkExists("favorites.addFolder")) { + if (_checkExists("favorites.addFolder")) { addFolder = (name) async { try { await JsEngine().runCode(""" @@ -741,7 +743,7 @@ class ComicSourceParser { } }; } - if(_checkExists("favorites.deleteFolder")) { + if (_checkExists("favorites.deleteFolder")) { deleteFolder = (key) async { try { await JsEngine().runCode(""" diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 6f04bfd..16dca69 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -1574,7 +1574,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> { const SizedBox(width: 16), Expanded( child: FilledButton( - onPressed: () { + onPressed: selected.isEmpty ? null : () { widget.finishSelect(selected); context.pop(); }, @@ -1585,7 +1585,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> { ], ), ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 4), + SizedBox(height: MediaQuery.of(context).padding.bottom), ], ), ); diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index d6ff26e..1079dc8 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -451,10 +451,11 @@ class _ComicSourceListState extends State<_ComicSourceList> { itemBuilder: (context, index) { var key = json![index]["key"]; var action = currentKey.contains(key) - ? const Icon(Icons.check) + ? const Icon(Icons.check, size: 22).paddingRight(8) : Tooltip( message: "Add", child: IconButton( + color: context.colorScheme.primary, icon: const Icon(Icons.add), onPressed: () async { await widget.onAdd( From afa320e8635c257bc7c8f591b8caf2d063a233cd Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 15:34:05 +0800 Subject: [PATCH 06/12] add 'Long press to zoom' setting --- assets/translation.json | 6 ++++-- lib/foundation/appdata.dart | 1 + lib/pages/reader/images.dart | 12 ++++++++++++ lib/pages/settings/reader.dart | 7 +++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index d8e93c8..e89eb09 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -167,7 +167,8 @@ "Download All": "下载全部", "Order": "顺序", "minAppVersion @version is required": "需要最低App版本 @version", - "Remove": "移除" + "Remove": "移除", + "Long press to zoom": "长按缩放" }, "zh_TW": { "Home": "首頁", @@ -337,6 +338,7 @@ "Download All": "下載全部", "Order": "順序", "minAppVersion @version is required": "需要最低App版本 @version", - "Remove": "移除" + "Remove": "移除", + "Long press to zoom": "長按縮放" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index e071ef7..50c941c 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -111,6 +111,7 @@ class _Settings with ChangeNotifier { 'language': 'system', // system, zh-CN, zh-TW, en-US 'cacheSize': 2048, // in MB 'downloadThreads': 5, + 'enableLongPressToZoom': true, }; operator [](String key) { diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index f1c427f..578b893 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -223,6 +223,9 @@ class _GalleryModeState extends State<_GalleryMode> @override void handleLongPressDown(Offset location) { + if(!appdata.settings['enableLongPressToZoom']) { + return; + } var photoViewController = photoViewControllers[reader.page]!; double target = photoViewController.getInitialScale!.call()! * 1.75; var size = MediaQuery.of(context).size; @@ -234,6 +237,9 @@ class _GalleryModeState extends State<_GalleryMode> @override void handleLongPressUp(Offset location) { + if(!appdata.settings['enableLongPressToZoom']) { + return; + } var photoViewController = photoViewControllers[reader.page]!; double target = photoViewController.getInitialScale!.call()!; photoViewController.animateScale?.call(target); @@ -509,6 +515,9 @@ class _ContinuousModeState extends State<_ContinuousMode> @override void handleLongPressDown(Offset location) { + if(!appdata.settings['enableLongPressToZoom']) { + return; + } double target = photoViewController.getInitialScale!.call()! * 1.75; var size = MediaQuery.of(context).size; photoViewController.animateScale?.call( @@ -519,6 +528,9 @@ class _ContinuousModeState extends State<_ContinuousMode> @override void handleLongPressUp(Offset location) { + if(!appdata.settings['enableLongPressToZoom']) { + return; + } double target = photoViewController.getInitialScale!.call()!; photoViewController.animateScale?.call(target); } diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 4704d24..e8cb88d 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -54,6 +54,13 @@ class _ReaderSettingsState extends State { widget.onChanged?.call("autoPageTurningInterval"); }, ).toSliver(), + _SwitchSetting( + title: 'Long press to zoom'.tl, + settingKey: 'enableLongPressToZoom', + onChanged: () { + widget.onChanged?.call('enableLongPressToZoom'); + }, + ).toSliver(), ], ); } From 7991f1a3856d1303a08301292747443cc73b1987 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 16:04:10 +0800 Subject: [PATCH 07/12] check updates on start --- assets/translation.json | 6 ++-- lib/components/message.dart | 7 +++-- lib/foundation/appdata.dart | 1 + lib/main.dart | 19 ++++++++++++ lib/pages/comic_source_page.dart | 13 +++++--- lib/pages/settings/about.dart | 52 +++++++++++++++++--------------- 6 files changed, 64 insertions(+), 34 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index e89eb09..0df06c2 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -168,7 +168,8 @@ "Order": "顺序", "minAppVersion @version is required": "需要最低App版本 @version", "Remove": "移除", - "Long press to zoom": "长按缩放" + "Long press to zoom": "长按缩放", + "Updates Available": "更新可用" }, "zh_TW": { "Home": "首頁", @@ -339,6 +340,7 @@ "Order": "順序", "minAppVersion @version is required": "需要最低App版本 @version", "Remove": "移除", - "Long press to zoom": "長按縮放" + "Long press to zoom": "長按縮放", + "Updates Available": "更新可用" } } \ No newline at end of file diff --git a/lib/components/message.dart b/lib/components/message.dart index 642dae6..d1fb8aa 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -129,13 +129,14 @@ void showDialogMessage(BuildContext context, String title, String message) { ); } -void showConfirmDialog({ +Future showConfirmDialog({ required BuildContext context, required String title, required String content, required void Function() onConfirm, + String confirmText = "Confirm", }) { - showDialog( + return showDialog( context: context, builder: (context) => ContentDialog( title: title, @@ -146,7 +147,7 @@ void showConfirmDialog({ context.pop(); onConfirm(); }, - child: Text("Confirm".tl), + child: Text(confirmText.tl), ), ], ), diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 50c941c..817aa96 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -112,6 +112,7 @@ class _Settings with ChangeNotifier { 'cacheSize': 2048, // in MB 'downloadThreads': 5, 'enableLongPressToZoom': true, + 'checkUpdateOnStart': true, }; operator [](String key) { diff --git a/lib/main.dart b/lib/main.dart index 9a7be51..9286fc0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:venera/foundation/log.dart'; +import 'package:venera/pages/comic_source_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:window_manager/window_manager.dart'; import 'components/components.dart'; @@ -63,6 +65,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { @override void initState() { + checkUpdates(); App.registerForceRebuild(forceRebuild); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.initState(); @@ -163,6 +166,22 @@ class _MyAppState extends State { }, ); } + + 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 { diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 1079dc8..9683804 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -14,11 +14,11 @@ import 'package:venera/utils/translations.dart'; class ComicSourcePage extends StatefulWidget { const ComicSourcePage({super.key}); - static void checkComicSourceUpdate([bool showLoading = false]) async { + static Future checkComicSourceUpdate([bool implicit = false]) async { if (ComicSource.all().isEmpty) { return; } - var controller = showLoading ? showLoadingDialog(App.rootContext) : null; + var controller = implicit ? null : showLoadingDialog(App.rootContext); var dio = AppDio(); var res = await dio.get( "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json"); @@ -40,7 +40,9 @@ class ComicSourcePage extends StatefulWidget { } controller?.close(); if (shouldUpdate.isEmpty) { - App.rootContext.showMessage(message: "No Update Available".tl); + if(!implicit) { + App.rootContext.showMessage(message: "No Update Available".tl); + } return; } var msg = ""; @@ -48,10 +50,11 @@ class ComicSourcePage extends StatefulWidget { msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n"; } msg = msg.trim(); - showConfirmDialog( + await showConfirmDialog( context: App.rootContext, title: "Updates Available".tl, content: msg, + confirmText: "Update", onConfirm: () { for (var key in shouldUpdate) { var source = ComicSource.find(key); @@ -104,7 +107,7 @@ class _BodyState extends State<_Body> { child: ListTile( leading: const Icon(Icons.update_outlined), title: Text("Check updates".tl), - onTap: () => ComicSourcePage.checkComicSourceUpdate(true), + onTap: () => ComicSourcePage.checkComicSourceUpdate(false), trailing: const Icon(Icons.arrow_right), ), ); diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index f9a46da..f68ef69 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -53,30 +53,7 @@ class _AboutSettingsState extends State { setState(() { isCheckingUpdate = true; }); - checkUpdate().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); - } + checkUpdateUi().then((value) { setState(() { isCheckingUpdate = false; }); @@ -108,6 +85,33 @@ Future checkUpdate() async { return false; } +Future 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 bool _compareVersion(String version1, String version2) { var v1 = version1.split("."); From b09e2e6f12ddaaf7d8a750525510ef82eb12a1e0 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 16:46:01 +0800 Subject: [PATCH 08/12] use rhttp --- android/app/build.gradle | 3 ++ lib/main.dart | 4 ++ lib/network/app_dio.dart | 32 +++++++++++--- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 56 ++++++++++++++++++++++--- pubspec.yaml | 2 + windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 88 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0394805..59f3a36 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -75,6 +75,9 @@ android { buildTypes { release { + ndk { + abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + } signingConfig signingConfigs.release applicationVariants.all { variant -> variant.outputs.all { output -> diff --git a/lib/main.dart b/lib/main.dart index 9286fc0..34488ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,9 @@ import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:rhttp/rhttp.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/settings/settings_page.dart'; @@ -20,6 +22,8 @@ void main(List args) { return; } runZonedGuarded(() async { + await Rhttp.init(); + AppDio.init(); WidgetsFlutterBinding.ensureInitialized(); await init(); if (App.isAndroid) { diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index 0a060a1..b96e292 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -2,8 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:dio/io.dart'; +import 'package:dio_compatibility_layer/dio_compatibility_layer.dart'; import 'package:flutter/services.dart'; +import 'package:rhttp/rhttp.dart' as rhttp; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/cache.dart'; @@ -106,10 +107,16 @@ class MyLogInterceptor implements Interceptor { class AppDio with DioMixin { String? _proxy = proxy; + static rhttp.RhttpCompatibleClient? _compatibleClient; + + static void init() { + _compatibleClient = rhttp.RhttpCompatibleClient.createSync(); + } + AppDio([BaseOptions? options]) { this.options = options ?? BaseOptions(); interceptors.add(MyLogInterceptor()); - httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient); + httpClientAdapter = ConversionLayerAdapter(_compatibleClient!); interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); interceptors.add(NetworkCacheManager()); interceptors.add(CloudflareInterceptor()); @@ -136,8 +143,9 @@ class AppDio with DioMixin { static String? proxy; static Future getProxy() async { - if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") + if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") { return null; + } if (appdata.settings['proxy'] != "system") return appdata.settings['proxy']; String res; @@ -187,10 +195,22 @@ class AppDio with DioMixin { }) async { proxy = await getProxy(); if (_proxy != proxy) { + Log.info("Network", "Proxy changed to $proxy"); _proxy = proxy; - (httpClientAdapter as IOHttpClientAdapter).close(); - httpClientAdapter = - IOHttpClientAdapter(createHttpClient: createHttpClient); + (httpClientAdapter as ConversionLayerAdapter).close(); + _compatibleClient = await rhttp.RhttpCompatibleClient.create( + settings: rhttp.ClientSettings( + proxySettings: proxy == null + ? const rhttp.ProxySettings.noProxy() + : rhttp.ProxySettings.proxy(proxy!), + timeoutSettings: const rhttp.TimeoutSettings( + connectTimeout: Duration(seconds: 15), + keepAliveTimeout: Duration(seconds: 60), + keepAlivePing: Duration(seconds: 30), + ), + ), + ); + httpClientAdapter = ConversionLayerAdapter(_compatibleClient!); } Log.info( "Network", diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6558394..819bf88 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST lodepng_flutter + rhttp zip_flutter ) diff --git a/pubspec.lock b/pubspec.lock index 5f59f6f..2f65d65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -130,6 +138,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.7.0" + dio_compatibility_layer: + dependency: "direct main" + description: + name: dio_compatibility_layer + sha256: bb7ea1dd6fe98b8f5e3d90da408802fc3abf14d4485416e882cf1b2c8fb4b209 + url: "https://pub.dev" + source: hosted + version: "0.1.0" dio_web_adapter: dependency: transitive description: @@ -341,6 +357,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct dev" description: flutter @@ -368,6 +392,14 @@ packages: description: flutter source: sdk 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: dependency: transitive description: @@ -424,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -459,11 +499,9 @@ packages: lodepng_flutter: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "5223cf4ce8aad1c2315db0093db3cc5c6c7191a8" - url: "https://github.com/venera-app/lodepng_flutter" - source: git + path: "C:\\Users\\wgh19\\IdeaProjects\\lodepng_flutter" + relative: false + source: path version: "0.0.1" matcher: dependency: transitive @@ -594,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c77fc04..29b2716 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,8 @@ dependencies: lodepng_flutter: git: url: https://github.com/venera-app/lodepng_flutter + rhttp: 0.9.1 + dio_compatibility_layer: ^0.1.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ff62697..d92ca11 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST lodepng_flutter + rhttp zip_flutter ) From a6608b6fa20718a56e77b0babb89a3858453f1c2 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 16:50:32 +0800 Subject: [PATCH 09/12] improve ui --- lib/main.dart | 2 +- lib/pages/comic_source_page.dart | 4 ++-- pubspec.lock | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 34488ae..241879a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -178,7 +178,7 @@ class _MyAppState extends State { var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0; var now = DateTime.now().millisecondsSinceEpoch; if(now - lastCheck < 24 * 60 * 60 * 1000) { - // return; + return; } appdata.implicitData['lastCheckUpdate'] = now; appdata.writeImplicitData(); diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 9683804..95c5f67 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -454,10 +454,10 @@ class _ComicSourceListState extends State<_ComicSourceList> { itemBuilder: (context, index) { var key = json![index]["key"]; var action = currentKey.contains(key) - ? const Icon(Icons.check, size: 22).paddingRight(8) + ? const Icon(Icons.check, size: 20).paddingRight(8) : Tooltip( message: "Add", - child: IconButton( + child: Button.icon( color: context.colorScheme.primary, icon: const Icon(Icons.add), onPressed: () async { diff --git a/pubspec.lock b/pubspec.lock index 2f65d65..1eb95bd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -499,9 +499,11 @@ packages: lodepng_flutter: dependency: "direct main" description: - path: "C:\\Users\\wgh19\\IdeaProjects\\lodepng_flutter" - relative: false - source: path + path: "." + ref: HEAD + resolved-ref: c9584f890f54c26b98ca4f465a2346024a528b31 + url: "https://github.com/venera-app/lodepng_flutter" + source: git version: "0.0.1" matcher: dependency: transitive From 96c75300d00adea99c15e7f7c0faae0141ba7a9a Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 17:03:19 +0800 Subject: [PATCH 10/12] update info --- README.md | 7 +++++++ pubspec.yaml | 1 + 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index a854df8..abe4797 100644 --- a/README.md +++ b/README.md @@ -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 - 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 See [venera-configs](https://github.com/venera-app/venera-configs) diff --git a/pubspec.yaml b/pubspec.yaml index 29b2716..7f14035 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: lodepng_flutter: git: url: https://github.com/venera-app/lodepng_flutter + ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 rhttp: 0.9.1 dio_compatibility_layer: ^0.1.0 From 98b9e6e9d9c99c8298992fdbd05ca4fe3f03e79d Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 20:18:10 +0800 Subject: [PATCH 11/12] fix http --- lib/main.dart | 1 - lib/network/app_dio.dart | 106 +++++++++++++++++++++++++++++++-------- pubspec.lock | 12 +---- pubspec.yaml | 1 - 4 files changed, 86 insertions(+), 34 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 241879a..7351392 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,7 +23,6 @@ void main(List args) { } runZonedGuarded(() async { await Rhttp.init(); - AppDio.init(); WidgetsFlutterBinding.ensureInitialized(); await init(); if (App.isAndroid) { diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index b96e292..dbbc7bc 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:dio_compatibility_layer/dio_compatibility_layer.dart'; import 'package:flutter/services.dart'; import 'package:rhttp/rhttp.dart' as rhttp; import 'package:venera/foundation/appdata.dart'; @@ -107,16 +106,10 @@ class MyLogInterceptor implements Interceptor { class AppDio with DioMixin { String? _proxy = proxy; - static rhttp.RhttpCompatibleClient? _compatibleClient; - - static void init() { - _compatibleClient = rhttp.RhttpCompatibleClient.createSync(); - } - AppDio([BaseOptions? options]) { this.options = options ?? BaseOptions(); interceptors.add(MyLogInterceptor()); - httpClientAdapter = ConversionLayerAdapter(_compatibleClient!); + httpClientAdapter = RHttpAdapter(const rhttp.ClientSettings()); interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); interceptors.add(NetworkCacheManager()); interceptors.add(CloudflareInterceptor()); @@ -197,20 +190,11 @@ class AppDio with DioMixin { if (_proxy != proxy) { Log.info("Network", "Proxy changed to $proxy"); _proxy = proxy; - (httpClientAdapter as ConversionLayerAdapter).close(); - _compatibleClient = await rhttp.RhttpCompatibleClient.create( - settings: rhttp.ClientSettings( - proxySettings: proxy == null - ? const rhttp.ProxySettings.noProxy() - : rhttp.ProxySettings.proxy(proxy!), - timeoutSettings: const rhttp.TimeoutSettings( - connectTimeout: Duration(seconds: 15), - keepAliveTimeout: Duration(seconds: 60), - keepAlivePing: Duration(seconds: 30), - ), - ), - ); - httpClientAdapter = ConversionLayerAdapter(_compatibleClient!); + httpClientAdapter = RHttpAdapter(rhttp.ClientSettings( + proxySettings: proxy == null + ? const rhttp.ProxySettings.noProxy() + : rhttp.ProxySettings.proxy(proxy!), + )); } Log.info( "Network", @@ -229,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 fetch( + RequestOptions options, + Stream? requestStream, + Future? 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 = >{}; + 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 = []; + 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, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 1eb95bd..fdf85c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -138,14 +138,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.7.0" - dio_compatibility_layer: - dependency: "direct main" - description: - name: dio_compatibility_layer - sha256: bb7ea1dd6fe98b8f5e3d90da408802fc3abf14d4485416e882cf1b2c8fb4b209 - url: "https://pub.dev" - source: hosted - version: "0.1.0" dio_web_adapter: dependency: transitive description: @@ -500,8 +492,8 @@ packages: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: c9584f890f54c26b98ca4f465a2346024a528b31 + ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 + resolved-ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 url: "https://github.com/venera-app/lodepng_flutter" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 7f14035..c71cbf9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,6 @@ dependencies: url: https://github.com/venera-app/lodepng_flutter ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53 rhttp: 0.9.1 - dio_compatibility_layer: ^0.1.0 dev_dependencies: flutter_test: From af371df2a491f06de05b4b93d520915d614f8590 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 5 Nov 2024 22:53:01 +0800 Subject: [PATCH 12/12] update windows build script --- windows/build.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/build.iss b/windows/build.iss index 949b5d0..905d0cf 100644 --- a/windows/build.iss +++ b/windows/build.iss @@ -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\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\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion 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