From 1c1f4180195d66957f4fa57b8a0ed3b9b211a981 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 18 Jan 2025 22:55:34 +0800 Subject: [PATCH 01/19] fix UI api --- lib/foundation/js_engine.dart | 3 +++ lib/pages/comic_source_page.dart | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 9accea8..fd87135 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -714,6 +714,8 @@ mixin class _JsUiApi { var content = message['content']; var actions = {}; for (var action in message['actions']) { + // [message] will be released after the method call, causing the action to be invalid, so we need to duplicate it + (action['callback'] as JSInvokable).dup(); actions[action['text']] = JSAutoFreeFunction(action['callback']); } showDialog(context: App.rootContext, builder: (context) { @@ -724,6 +726,7 @@ mixin class _JsUiApi { return TextButton( onPressed: () { entry.value.call([]); + context.pop(); }, child: Text(entry.key), ); diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index dd43ef1..e8c43a6 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -246,7 +246,7 @@ class _BodyState extends State<_Body> { ), ); } else if (type == "callback") { - yield _CallbackSetting(setting: item); + yield _CallbackSetting(setting: item, sourceKey: source.key); } } catch (e, s) { Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); @@ -682,10 +682,12 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { } class _CallbackSetting extends StatefulWidget { - const _CallbackSetting({required this.setting}); + const _CallbackSetting({required this.setting, required this.sourceKey}); final MapEntry> setting; + final String sourceKey; + @override State<_CallbackSetting> createState() => _CallbackSettingState(); } @@ -719,11 +721,11 @@ class _CallbackSettingState extends State<_CallbackSetting> { @override Widget build(BuildContext context) { return ListTile( - title: Text(title.ts(key)), + title: Text(title.ts(widget.sourceKey)), trailing: Button.normal( onPressed: onClick, isLoading: isLoading, - child: Text(buttonText.ts(key)), + child: Text(buttonText.ts(widget.sourceKey)), ).fixHeight(32), ); } From c6ec38632ffcb141eb03d37741c649e44e1cfaec Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 19 Jan 2025 10:05:08 +0800 Subject: [PATCH 02/19] fix data sync --- lib/foundation/appdata.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 320dc10..54e9bb7 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -90,13 +90,15 @@ class _Appdata { /// Sync data from another device void syncData(Map data) { - for (var key in data.keys) { - if (_disableSync.contains(key)) { - continue; + if (data['settings'] is Map) { + var settings = data['settings'] as Map; + for (var key in settings.keys) { + if (!_disableSync.contains(key)) { + this.settings[key] = settings[key]; + } } - settings[key] = data[key]; } - searchHistory = List.from(data['searchHistory']); + searchHistory = List.from(data['searchHistory'] ?? []); saveData(); } From 5e3ff48d352861cd60818dbba5444ab120808077 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 19 Jan 2025 10:06:52 +0800 Subject: [PATCH 03/19] fix explore page --- lib/pages/explore_page.dart | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index d43e57b..47866c5 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -124,18 +124,7 @@ class _ExplorePageState extends State } return NetworkError( message: msg, - retry: () { - setState(() { - pages = ComicSource.all() - .map((e) => e.explorePages) - .expand((e) => e.map((e) => e.title)) - .toList(); - controller = TabController( - length: pages.length, - vsync: this, - ); - }); - }, + retry: onSettingsChanged, withAppbar: false, ); } From 51b7df02e7c1fa5d1b7c0bea550a7f7d7e3f3e12 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 19 Jan 2025 20:36:17 +0800 Subject: [PATCH 04/19] Improve ui api --- assets/init.js | 34 ++++++- lib/components/js_ui.dart | 183 ++++++++++++++++++++++++++++++++++ lib/foundation/js_engine.dart | 53 +--------- 3 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 lib/components/js_ui.dart diff --git a/assets/init.js b/assets/init.js index 5e7b234..ef6b393 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1205,6 +1205,10 @@ class Image { } } +/** + * UI related apis + * @since 1.2.0 + */ let UI = { /** * Show a message @@ -1222,7 +1226,8 @@ let UI = { * Show a dialog. Any action will close the dialog. * @param title {string} * @param content {string} - * @param actions {{text:string, callback: () => void}[]} + * @param actions {{text:string, callback: () => void | Promise, style: "text"|"filled"|"danger"}[]} - If callback returns a promise, the button will show a loading indicator until the promise is resolved. + * @since 1.2.1 */ showDialog: (title, content, actions) => { sendMessage({ @@ -1245,4 +1250,31 @@ let UI = { url: url, }) }, + + /** + * Show a loading dialog. + * @param onCancel {() => void | null | undefined} - Called when the loading dialog is canceled. If [onCancel] is null, the dialog cannot be canceled by the user. + * @returns {number} - A number that can be used to cancel the loading dialog. + * @since 1.2.1 + */ + showLoading: (onCancel) => { + return sendMessage({ + method: 'UI', + function: 'showLoading', + onCancel: onCancel + }) + }, + + /** + * Cancel a loading dialog. + * @param id {number} - returned by [showLoading] + * @since 1.2.1 + */ + cancelLoading: (id) => { + sendMessage({ + method: 'UI', + function: 'cancelLoading', + id: id + }) + } } \ No newline at end of file diff --git a/lib/components/js_ui.dart b/lib/components/js_ui.dart new file mode 100644 index 0000000..4da74cf --- /dev/null +++ b/lib/components/js_ui.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_qjs/flutter_qjs.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/js_engine.dart'; + +import 'components.dart'; + +mixin class JsUiApi { + final Map _loadingDialogControllers = {}; + + dynamic handleUIMessage(Map message) { + switch (message['function']) { + case 'showMessage': + var m = message['message']; + if (m.toString().isNotEmpty) { + App.rootContext.showMessage(message: m.toString()); + } + case 'showDialog': + _showDialog(message); + case 'launchUrl': + var url = message['url']; + if (url.toString().isNotEmpty) { + launchUrlString(url.toString()); + } + case 'showLoading': + var onCancel = message['onCancel']; + if (onCancel != null && onCancel is! JSInvokable) { + return; + } + return showLoading(onCancel); + case 'cancelLoading': + var id = message['id']; + if (id is int) { + cancelLoading(id); + } + } + } + + void _showDialog(Map message) { + BuildContext? dialogContext; + var title = message['title']; + var content = message['content']; + var actions = []; + for (var action in message['actions']) { + if (action['callback'] is! JSInvokable) { + continue; + } + var callback = action['callback'] as JSInvokable; + // [message] will be released after the method call, causing the action to be invalid, so we need to duplicate it + callback.dup(); + var text = action['text'].toString(); + var style = (action['style'] ?? 'text').toString(); + actions.add(_JSCallbackButton( + text: text, + callback: JSAutoFreeFunction(callback), + style: style, + onCallbackFinished: () { + dialogContext?.pop(); + }, + )); + } + if (actions.isEmpty) { + actions.add(TextButton( + onPressed: () { + dialogContext?.pop(); + }, + child: Text('OK'), + )); + } + showDialog( + context: App.rootContext, + builder: (context) { + dialogContext = context; + return ContentDialog( + title: title, + content: Text(content).paddingHorizontal(16), + actions: actions, + ); + }, + ).then((value) { + dialogContext = null; + }); + } + + int showLoading(JSInvokable? onCancel) { + onCancel?.dup(); + var func = onCancel == null ? null : JSAutoFreeFunction(onCancel); + var controller = showLoadingDialog( + App.rootContext, + barrierDismissible: onCancel != null, + allowCancel: onCancel != null, + onCancel: onCancel == null ? null : () { + func?.call([]); + }, + ); + var i = 0; + while (_loadingDialogControllers.containsKey(i)) { + i++; + } + _loadingDialogControllers[i] = controller; + return i; + } + + void cancelLoading(int id) { + var controller = _loadingDialogControllers.remove(id); + controller?.close(); + } +} + +class _JSCallbackButton extends StatefulWidget { + const _JSCallbackButton({ + required this.text, + required this.callback, + required this.style, + this.onCallbackFinished, + }); + + final JSAutoFreeFunction callback; + + final String text; + + final String style; + + final void Function()? onCallbackFinished; + + @override + State<_JSCallbackButton> createState() => _JSCallbackButtonState(); +} + +class _JSCallbackButtonState extends State<_JSCallbackButton> { + bool isLoading = false; + + void onClick() async { + if (isLoading) { + return; + } + var res = widget.callback.call([]); + if (res is Future) { + setState(() { + isLoading = true; + }); + await res; + setState(() { + isLoading = false; + }); + } + widget.onCallbackFinished?.call(); + } + + @override + Widget build(BuildContext context) { + return switch (widget.style) { + "filled" => FilledButton( + onPressed: onClick, + child: isLoading + ? CircularProgressIndicator(strokeWidth: 1.4) + .fixWidth(18) + .fixHeight(18) + : Text(widget.text), + ), + "danger" => FilledButton( + onPressed: onClick, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(context.colorScheme.error), + ), + child: isLoading + ? CircularProgressIndicator(strokeWidth: 1.4) + .fixWidth(18) + .fixHeight(18) + : Text(widget.text), + ), + _ => TextButton( + onPressed: onClick, + child: isLoading + ? CircularProgressIndicator(strokeWidth: 1.4) + .fixWidth(18) + .fixHeight(18) + : Text(widget.text), + ), + }; + } +} diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index fd87135..4cc59d8 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:math' as math; import 'package:crypto/crypto.dart'; import 'package:dio/io.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:html/parser.dart' as html; import 'package:html/dom.dart' as dom; @@ -20,9 +19,8 @@ import 'package:pointycastle/block/modes/cbc.dart'; import 'package:pointycastle/block/modes/cfb.dart'; import 'package:pointycastle/block/modes/ecb.dart'; import 'package:pointycastle/block/modes/ofb.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import 'package:uuid/uuid.dart'; -import 'package:venera/components/components.dart'; +import 'package:venera/components/js_ui.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/network/cookie_jar.dart'; @@ -42,7 +40,7 @@ class JavaScriptRuntimeException implements Exception { } } -class JsEngine with _JSEngineApi, _JsUiApi { +class JsEngine with _JSEngineApi, JsUiApi { factory JsEngine() => _cache ?? (_cache = JsEngine._create()); static JsEngine? _cache; @@ -156,7 +154,7 @@ class JsEngine with _JSEngineApi, _JsUiApi { case "delay": return Future.delayed(Duration(milliseconds: message["time"])); case "UI": - handleUIMessage(Map.from(message)); + return handleUIMessage(Map.from(message)); } } return null; @@ -690,48 +688,3 @@ class JSAutoFreeFunction { func.free(); }); } - -mixin class _JsUiApi { - void handleUIMessage(Map message) { - switch (message['function']) { - case 'showMessage': - var m = message['message']; - if (m.toString().isNotEmpty) { - App.rootContext.showMessage(message: m.toString()); - } - case 'showDialog': - _showDialog(message); - case 'launchUrl': - var url = message['url']; - if (url.toString().isNotEmpty) { - launchUrlString(url.toString()); - } - } - } - - void _showDialog(Map message) { - var title = message['title']; - var content = message['content']; - var actions = {}; - for (var action in message['actions']) { - // [message] will be released after the method call, causing the action to be invalid, so we need to duplicate it - (action['callback'] as JSInvokable).dup(); - actions[action['text']] = JSAutoFreeFunction(action['callback']); - } - showDialog(context: App.rootContext, builder: (context) { - return ContentDialog( - title: title, - content: Text(content).paddingHorizontal(16), - actions: actions.entries.map((entry) { - return TextButton( - onPressed: () { - entry.value.call([]); - context.pop(); - }, - child: Text(entry.key), - ); - }).toList(), - ); - }); - } -} From 63346396e0ef032b47bdbf47717b77823ffd699d Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 19 Jan 2025 20:55:53 +0800 Subject: [PATCH 05/19] Add input dialog --- assets/init.js | 15 +++++++ lib/components/js_ui.dart | 40 ++++++++++++++++--- .../image_provider/reader_image.dart | 13 +++--- lib/foundation/js_engine.dart | 3 +- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/assets/init.js b/assets/init.js index ef6b393..cf4b3b4 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1276,5 +1276,20 @@ let UI = { function: 'cancelLoading', id: id }) + }, + + /** + * Show an input dialog + * @param title {string} + * @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message. + * @returns {string | null} - The input value. If the dialog is canceled, return null. + */ + showInputDialog: (title, validator) => { + return sendMessage({ + method: 'UI', + function: 'showInputDialog', + title: title, + validator: validator + }) } } \ No newline at end of file diff --git a/lib/components/js_ui.dart b/lib/components/js_ui.dart index 4da74cf..2f5cffe 100644 --- a/lib/components/js_ui.dart +++ b/lib/components/js_ui.dart @@ -34,6 +34,12 @@ mixin class JsUiApi { if (id is int) { cancelLoading(id); } + case 'showInputDialog': + var title = message['title']; + var validator = message['validator']; + if (title is! String) return; + if (validator != null && validator is! JSInvokable) return; + return _showInputDialog(title, validator); } } @@ -47,8 +53,6 @@ mixin class JsUiApi { continue; } var callback = action['callback'] as JSInvokable; - // [message] will be released after the method call, causing the action to be invalid, so we need to duplicate it - callback.dup(); var text = action['text'].toString(); var style = (action['style'] ?? 'text').toString(); actions.add(_JSCallbackButton( @@ -84,15 +88,16 @@ mixin class JsUiApi { } int showLoading(JSInvokable? onCancel) { - onCancel?.dup(); var func = onCancel == null ? null : JSAutoFreeFunction(onCancel); var controller = showLoadingDialog( App.rootContext, barrierDismissible: onCancel != null, allowCancel: onCancel != null, - onCancel: onCancel == null ? null : () { - func?.call([]); - }, + onCancel: onCancel == null + ? null + : () { + func?.call([]); + }, ); var i = 0; while (_loadingDialogControllers.containsKey(i)) { @@ -106,6 +111,29 @@ mixin class JsUiApi { var controller = _loadingDialogControllers.remove(id); controller?.close(); } + + Future _showInputDialog(String title, JSInvokable? validator) async { + String? result; + var func = validator == null ? null : JSAutoFreeFunction(validator); + await showInputDialog( + context: App.rootContext, + title: title, + onConfirm: (v) { + if (func != null) { + var res = func.call([v]); + if (res != null) { + return res.toString(); + } else { + result = v; + } + } else { + result = v; + } + return null; + }, + ); + return result; + } } class _JSCallbackButton extends StatefulWidget { diff --git a/lib/foundation/image_provider/reader_image.dart b/lib/foundation/image_provider/reader_image.dart index 9c93a95..c392f53 100644 --- a/lib/foundation/image_provider/reader_image.dart +++ b/lib/foundation/image_provider/reader_image.dart @@ -63,7 +63,8 @@ class ReaderImageProvider })() '''); if (func is JSInvokable) { - var result = func.invoke([imageBytes, cid, eid, page, sourceKey]); + var autoFreeFunc = JSAutoFreeFunction(func); + var result = autoFreeFunc([imageBytes, cid, eid, page, sourceKey]); if (result is Uint8List) { imageBytes = result; } else if (result is Future) { @@ -76,9 +77,9 @@ class ReaderImageProvider if (image is Uint8List) { imageBytes = image; } else if (image is Future) { - JSInvokable? onCancel; + JSAutoFreeFunction? onCancel; if (result['onCancel'] is JSInvokable) { - onCancel = result['onCancel']; + onCancel = JSAutoFreeFunction(result['onCancel']); } if (onCancel == null) { var futureImage = await image; @@ -96,9 +97,7 @@ class ReaderImageProvider checkStop(); } catch(e) { - onCancel.invoke([]); - onCancel.free(); - func.free(); + onCancel([]); rethrow; } await Future.delayed(Duration(milliseconds: 50)); @@ -107,10 +106,8 @@ class ReaderImageProvider imageBytes = futureImage; } } - onCancel?.free(); } } - func.free(); } } return imageBytes!; diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 4cc59d8..eee6a6c 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -677,6 +677,7 @@ class JSAutoFreeFunction { /// Automatically free the function when it's not used anymore JSAutoFreeFunction(this.func) { + func.dup(); finalizer.attach(this, func); } @@ -685,6 +686,6 @@ class JSAutoFreeFunction { } static final finalizer = Finalizer((func) { - func.free(); + func.destroy(); }); } From 7b7710b441202ac04970b46595b9d922f34ed65c Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 19 Jan 2025 22:35:00 +0800 Subject: [PATCH 06/19] Update flutter_qjs --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 8270f48..3104de5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,8 +417,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9c99ac258a11f8e91761a5466a190efba3ca64af" - resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af" + ref: "67496e2158380a8a715254bfca4899fe9b718968" + resolved-ref: "67496e2158380a8a715254bfca4899fe9b718968" url: "https://github.com/wgh136/flutter_qjs" source: git version: "0.3.7" diff --git a/pubspec.yaml b/pubspec.yaml index 8c25efe..11be869 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs - ref: 9c99ac258a11f8e91761a5466a190efba3ca64af + ref: 67496e2158380a8a715254bfca4899fe9b718968 crypto: ^3.0.6 dio: ^5.7.0 html: ^0.15.5 From d88ae57320a6e5782b6cae8c0d04399d3ce5684c Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 15:02:36 +0800 Subject: [PATCH 07/19] Add select dialog --- assets/init.js | 17 +++++++++++ lib/components/js_ui.dart | 39 +++++++++++++++++++++++--- lib/components/message.dart | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/assets/init.js b/assets/init.js index cf4b3b4..372231a 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1291,5 +1291,22 @@ let UI = { title: title, validator: validator }) + }, + + /** + * Show a select dialog + * @param title {string} + * @param options {string[]} + * @param initialIndex {number?} + * @returns {number | null} - The selected index. If the dialog is canceled, return null. + */ + showSelectDialog: (title, options, initialIndex) => { + return sendMessage({ + method: 'UI', + function: 'showSelectDialog', + title: title, + options: options, + initialIndex: initialIndex + }) } } \ No newline at end of file diff --git a/lib/components/js_ui.dart b/lib/components/js_ui.dart index 2f5cffe..16b238a 100644 --- a/lib/components/js_ui.dart +++ b/lib/components/js_ui.dart @@ -28,11 +28,11 @@ mixin class JsUiApi { if (onCancel != null && onCancel is! JSInvokable) { return; } - return showLoading(onCancel); + return _showLoading(onCancel); case 'cancelLoading': var id = message['id']; if (id is int) { - cancelLoading(id); + _cancelLoading(id); } case 'showInputDialog': var title = message['title']; @@ -40,6 +40,18 @@ mixin class JsUiApi { if (title is! String) return; if (validator != null && validator is! JSInvokable) return; return _showInputDialog(title, validator); + case 'showSelectDialog': + var title = message['title']; + var options = message['options']; + var initialIndex = message['initialIndex']; + if (title is! String) return; + if (options is! List) return; + if (initialIndex != null && initialIndex is! int) return; + return _showSelectDialog( + title, + options.whereType().toList(), + initialIndex, + ); } } @@ -87,7 +99,7 @@ mixin class JsUiApi { }); } - int showLoading(JSInvokable? onCancel) { + int _showLoading(JSInvokable? onCancel) { var func = onCancel == null ? null : JSAutoFreeFunction(onCancel); var controller = showLoadingDialog( App.rootContext, @@ -107,7 +119,7 @@ mixin class JsUiApi { return i; } - void cancelLoading(int id) { + void _cancelLoading(int id) { var controller = _loadingDialogControllers.remove(id); controller?.close(); } @@ -134,6 +146,25 @@ mixin class JsUiApi { ); return result; } + + Future _showSelectDialog( + String title, + List options, + int? initialIndex, + ) { + if (options.isEmpty) { + return Future.value(null); + } + if (initialIndex != null && + (initialIndex >= options.length || initialIndex < 0)) { + initialIndex = null; + } + return showSelectDialog( + title: title, + options: options, + initialIndex: initialIndex, + ); + } } class _JSCallbackButton extends StatefulWidget { diff --git a/lib/components/message.dart b/lib/components/message.dart index cfa07ff..42b4374 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -402,3 +402,59 @@ void showInfoDialog({ }, ); } + +Future showSelectDialog({ + required String title, + required List options, + int? initialIndex, +}) async { + int? current = initialIndex; + + await showDialog( + context: App.rootContext, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return ContentDialog( + title: title, + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Select( + current: current == null ? "" : options[current!], + values: options, + minWidth: 156, + onTap: (i) { + setState(() { + current = i; + }); + }, + ) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + current = null; + context.pop(); + }, + child: Text('Cancel'.tl), + ), + FilledButton( + onPressed: current == null + ? null + : context.pop, + child: Text('Confirm'.tl), + ), + ], + ); + }, + ); + }, + ); + + return current; +} From 27e7356721db08eaf29f382ec567b12598c50519 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 15:09:48 +0800 Subject: [PATCH 08/19] Upgrade to flutter 3.27.2 --- pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 3104de5..213842c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1156,4 +1156,4 @@ packages: version: "0.0.6" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.1" + flutter: ">=3.27.2" diff --git a/pubspec.yaml b/pubspec.yaml index 11be869..4720478 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.2.0+120 environment: sdk: '>=3.6.0 <4.0.0' - flutter: 3.27.1 + flutter: 3.27.2 dependencies: flutter: From 6033a3cde99ca7857ebdf101a3e883aa32211612 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 15:18:16 +0800 Subject: [PATCH 09/19] Add app api --- assets/init.js | 34 ++++++++++++++++++++++++++++++++++ lib/foundation/js_engine.dart | 4 ++++ 2 files changed, 38 insertions(+) diff --git a/assets/init.js b/assets/init.js index 372231a..e5cd807 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1309,4 +1309,38 @@ let UI = { initialIndex: initialIndex }) } +} + +/** + * App related apis + * @since 1.2.1 + */ +let APP = { + /** + * Get the app version + * @returns {string} - The app version + */ + get version() { + return appVersion // defined in the engine + }, + + /** + * Get current app locale + * @returns {string} - The app locale, in the format of [languageCode]_[countryCode] + */ + get locale() { + return sendMessage({ + method: 'getLocale' + }) + }, + + /** + * Get current running platform + * @returns {string} - The platform name, "android", "ios", "windows", "macos", "linux" + */ + get platform() { + return sendMessage({ + method: 'getPlatform' + }) + } } \ No newline at end of file diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index eee6a6c..9a5e723 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -155,6 +155,10 @@ class JsEngine with _JSEngineApi, JsUiApi { return Future.delayed(Duration(milliseconds: message["time"])); case "UI": return handleUIMessage(Map.from(message)); + case "getLocale": + return "${App.locale.languageCode}-${App.locale.countryCode}"; + case "getPlatform": + return Platform.operatingSystem; } } return null; From e6b7f5b0145c43e55c3bf3f010e48da5b312f74f Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 19:15:06 +0800 Subject: [PATCH 10/19] Move help to GitHub --- assets/translation.json | 10 ---------- lib/pages/comic_source_page.dart | 2 +- lib/pages/home_page.dart | 34 ++------------------------------ 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index eb2d5c4..118dcf5 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -148,11 +148,6 @@ "Size in MB": "大小(MB)", "Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录", "Help": "帮助", - "A directory is considered as a comic only if it matches one of the following conditions:" : "只有当目录满足以下条件之一时,才被视为漫画:", - "1. The directory only contains image files." : "1. 目录只包含图片文件。", - "2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。", - "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。", - "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n", "Export as cbz": "导出为cbz", "Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)", "An archive file" : "一个归档文件", @@ -468,11 +463,6 @@ "Size in MB": "大小(MB)", "Select a directory which contains the comic directories." : "選擇一個包含漫畫文件夾的目錄", "Help": "幫助", - "A directory is considered as a comic only if it matches one of the following conditions:" : "只有當目錄滿足以下條件之一時,才被視為漫畫:", - "1. The directory only contains image files." : "1. 目錄只包含圖片文件。", - "2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。", - "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。", - "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n", "Export as cbz": "匯出為cbz", "Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)", "An archive file" : "一個歸檔文件", diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index e8c43a6..b500f09 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -419,7 +419,7 @@ class _BodyState extends State<_Body> { } void help() { - launchUrlString("https://github.com/venera-app/venera-configs"); + launchUrlString("https://github.com/venera-app/venera/blob/master/doc/comic_source.md"); } Future handleAddSource(String url) async { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index e9579c3..ffd8318 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sliver_tools/sliver_tools.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; @@ -535,38 +536,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { ], ), onPressed: () { - showDialog( - context: context, - barrierColor: Colors.black.toOpacity(0.2), - builder: (context) { - var help = ''; - help += - '${"A directory is considered as a comic only if it matches one of the following conditions:".tl}\n'; - help += '${'1. The directory only contains image files.'.tl}\n'; - help += - '${'2. The directory contains directories which contain image files. Each directory is considered as a chapter.'.tl}\n\n'; - help += - '${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n'; - help += - "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" - .tl; - help += - "If you import an EhViewer's database, program will automatically create folders according to the download label in that database." - .tl; - return ContentDialog( - title: "Help".tl, - content: Text(help).paddingHorizontal(16), - actions: [ - Button.filled( - child: Text("OK".tl), - onPressed: () { - context.pop(); - }, - ), - ], - ); - }, - ); + launchUrlString("https://github.com/venera-app/venera/blob/master/doc/import_comic.md"); }, ).fixWidth(90).paddingRight(8), Button.filled( From c334e4fa05a1e74e02cda92c4940ccab21fefee8 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 19:28:03 +0800 Subject: [PATCH 11/19] Add a setting for comic source list url --- assets/translation.json | 6 ++++-- lib/foundation/appdata.dart | 1 + lib/pages/comic_source_page.dart | 24 ++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 118dcf5..6493129 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -313,7 +313,8 @@ "Imported @a comics": "已导入 @a 本漫画", "New Version": "新版本", "@c updates": "@c 项更新", - "No updates": "无更新" + "No updates": "无更新", + "Set comic source list url": "设置漫画源列表URL" }, "zh_TW": { "Home": "首頁", @@ -629,6 +630,7 @@ "Imported @a comics": "已匯入 @a 部漫畫", "New Version": "新版本", "@c updates": "@c 項更新", - "No updates": "無更新" + "No updates": "無更新", + "Set comic source list url": "設置漫畫源列表URL" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 54e9bb7..1e083df 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -155,6 +155,7 @@ class _Settings with ChangeNotifier { 'customImageProcessing': defaultCustomImageProcessing, 'sni': true, 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese + 'comicSourceListUrl': "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json", }; operator [](String key) { diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index b500f09..dccc945 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -469,8 +469,7 @@ class _ComicSourceListState extends State<_ComicSourceList> { void load() async { var dio = AppDio(); - var res = await dio.get( - "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json"); + var res = await dio.get(appdata.settings['comicSourceListUrl']); if (res.statusCode != 200) { context.showMessage(message: "Network error".tl); return; @@ -485,6 +484,27 @@ class _ComicSourceListState extends State<_ComicSourceList> { Widget build(BuildContext context) { return PopUpWidgetScaffold( title: "Comic Source".tl, + tailing: [ + IconButton( + icon: Icon(Icons.settings), + onPressed: () async { + await showInputDialog( + context: context, + title: "Set comic source list url".tl, + initialValue: appdata.settings['comicSourceListUrl'], + onConfirm: (value) { + appdata.settings['comicSourceListUrl'] = value; + appdata.saveData(); + setState(() { + loading = true; + json = null; + }); + return null; + }, + ); + }, + ) + ], body: buildBody(), ); } From f2388c81e0813bdbf1d3774d53bc2c4df30a543b Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 20:21:43 +0800 Subject: [PATCH 12/19] Lower iOS version requirements --- ios/Podfile | 2 +- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index f2c33c6..8eaa995 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '15.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/pubspec.lock b/pubspec.lock index 213842c..80c395b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1150,10 +1150,10 @@ packages: dependency: "direct main" description: name: zip_flutter - sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646 + sha256: "0b68bb8b4161cf65afa43100f5c5a0335392cb0f31662e0022b797cfd08d5833" url: "https://pub.dev" source: hosted - version: "0.0.6" + version: "0.0.7" sdks: dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.2" diff --git a/pubspec.yaml b/pubspec.yaml index 4720478..e25223e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 - zip_flutter: ^0.0.6 + zip_flutter: ^0.0.7 lodepng_flutter: git: url: https://github.com/venera-app/lodepng_flutter From c3a09c887004a070ea5e1ecda6835b6ce82029a7 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 20:48:16 +0800 Subject: [PATCH 13/19] Update flutter_qjs --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 80c395b..7e41916 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,8 +417,8 @@ packages: dependency: "direct main" description: path: "." - ref: "67496e2158380a8a715254bfca4899fe9b718968" - resolved-ref: "67496e2158380a8a715254bfca4899fe9b718968" + ref: "4ca22a459139dec6c036ad4de078ddfe9093a570" + resolved-ref: "4ca22a459139dec6c036ad4de078ddfe9093a570" url: "https://github.com/wgh136/flutter_qjs" source: git version: "0.3.7" diff --git a/pubspec.yaml b/pubspec.yaml index e25223e..0c5f744 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs - ref: 67496e2158380a8a715254bfca4899fe9b718968 + ref: 4ca22a459139dec6c036ad4de078ddfe9093a570 crypto: ^3.0.6 dio: ^5.7.0 html: ^0.15.5 From 283afbc6d40f3dc2fb9ac4d7b16249cba1214f56 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 21:06:45 +0800 Subject: [PATCH 14/19] Improve ui api --- assets/init.js | 5 +++-- lib/components/js_ui.dart | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/assets/init.js b/assets/init.js index e5cd807..37ad17e 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1227,6 +1227,7 @@ let UI = { * @param title {string} * @param content {string} * @param actions {{text:string, callback: () => void | Promise, style: "text"|"filled"|"danger"}[]} - If callback returns a promise, the button will show a loading indicator until the promise is resolved. + * @returns {Promise} - Resolved when the dialog is closed. * @since 1.2.1 */ showDialog: (title, content, actions) => { @@ -1282,7 +1283,7 @@ let UI = { * Show an input dialog * @param title {string} * @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message. - * @returns {string | null} - The input value. If the dialog is canceled, return null. + * @returns {Promise} - The input value. If the dialog is canceled, return null. */ showInputDialog: (title, validator) => { return sendMessage({ @@ -1298,7 +1299,7 @@ let UI = { * @param title {string} * @param options {string[]} * @param initialIndex {number?} - * @returns {number | null} - The selected index. If the dialog is canceled, return null. + * @returns {Promise} - The selected index. If the dialog is canceled, return null. */ showSelectDialog: (title, options, initialIndex) => { return sendMessage({ diff --git a/lib/components/js_ui.dart b/lib/components/js_ui.dart index 16b238a..04068e5 100644 --- a/lib/components/js_ui.dart +++ b/lib/components/js_ui.dart @@ -17,7 +17,7 @@ mixin class JsUiApi { App.rootContext.showMessage(message: m.toString()); } case 'showDialog': - _showDialog(message); + return _showDialog(message); case 'launchUrl': var url = message['url']; if (url.toString().isNotEmpty) { @@ -55,7 +55,7 @@ mixin class JsUiApi { } } - void _showDialog(Map message) { + Future _showDialog(Map message) { BuildContext? dialogContext; var title = message['title']; var content = message['content']; @@ -84,7 +84,7 @@ mixin class JsUiApi { child: Text('OK'), )); } - showDialog( + return showDialog( context: App.rootContext, builder: (context) { dialogContext = context; From 6ec4817dc12f63d49a409ae56395398c55306ddf Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 21:17:08 +0800 Subject: [PATCH 15/19] Fix ios and macos build --- pubspec.lock | 8 ++++---- pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7e41916..2582d34 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,8 +417,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4ca22a459139dec6c036ad4de078ddfe9093a570" - resolved-ref: "4ca22a459139dec6c036ad4de078ddfe9093a570" + ref: "6daeb70da041579d99b53baab3c23c7d9b234e66" + resolved-ref: "6daeb70da041579d99b53baab3c23c7d9b234e66" url: "https://github.com/wgh136/flutter_qjs" source: git version: "0.3.7" @@ -1150,10 +1150,10 @@ packages: dependency: "direct main" description: name: zip_flutter - sha256: "0b68bb8b4161cf65afa43100f5c5a0335392cb0f31662e0022b797cfd08d5833" + sha256: ea7fdc86c988174ef3bb80dc26e8e8bfdf634c55930e2d18d7e77e991acf0483 url: "https://pub.dev" source: hosted - version: "0.0.7" + version: "0.0.8" sdks: dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.2" diff --git a/pubspec.yaml b/pubspec.yaml index 0c5f744..4cfaee9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs - ref: 4ca22a459139dec6c036ad4de078ddfe9093a570 + ref: 6daeb70da041579d99b53baab3c23c7d9b234e66 crypto: ^3.0.6 dio: ^5.7.0 html: ^0.15.5 @@ -51,7 +51,7 @@ dependencies: sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 - zip_flutter: ^0.0.7 + zip_flutter: ^0.0.8 lodepng_flutter: git: url: https://github.com/venera-app/lodepng_flutter From 53b033258a44b54524b1598b56a4fc6f4598819c Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 20 Jan 2025 21:31:17 +0800 Subject: [PATCH 16/19] Fix ios build --- ios/Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile b/ios/Podfile index 8eaa995..fe71a27 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' From ff42c726fa1c212ac091a296be98417f71dcabca Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 21 Jan 2025 15:15:11 +0800 Subject: [PATCH 17/19] Fix network header --- lib/network/cache.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/network/cache.dart b/lib/network/cache.dart index a2b6465..18bb514 100644 --- a/lib/network/cache.dart +++ b/lib/network/cache.dart @@ -135,6 +135,8 @@ class NetworkCacheManager implements Interceptor { } static bool compareHeaders(Map a, Map b) { + a = Map.from(a); + b = Map.from(b); const shouldIgnore = [ 'cache-time', 'prevent-parallel', From ded90553633507a4cb771be95c20b0e276348fe3 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 21 Jan 2025 15:37:46 +0800 Subject: [PATCH 18/19] Update flutter_qjs --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 2582d34..bd2578e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,8 +417,8 @@ packages: dependency: "direct main" description: path: "." - ref: "6daeb70da041579d99b53baab3c23c7d9b234e66" - resolved-ref: "6daeb70da041579d99b53baab3c23c7d9b234e66" + ref: "598d50572a658f8e04775566fe3789954d9a01e3" + resolved-ref: "598d50572a658f8e04775566fe3789954d9a01e3" url: "https://github.com/wgh136/flutter_qjs" source: git version: "0.3.7" diff --git a/pubspec.yaml b/pubspec.yaml index 4cfaee9..465f209 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs - ref: 6daeb70da041579d99b53baab3c23c7d9b234e66 + ref: 598d50572a658f8e04775566fe3789954d9a01e3 crypto: ^3.0.6 dio: ^5.7.0 html: ^0.15.5 From 8645dda967cea87827eeff3f3fbc38a6a58fa2a3 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 21 Jan 2025 15:38:52 +0800 Subject: [PATCH 19/19] v1.2.1 --- lib/foundation/app.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index bb909cc..edbbedb 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.2.0"; + final version = "1.2.1"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index 465f209..c28528f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.2.0+120 +version: 1.2.1+121 environment: sdk: '>=3.6.0 <4.0.0'