diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index 0a175b8..7eed7e4 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/widgets.dart'; +import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; @@ -203,7 +204,7 @@ class ComicSource { final LikeCommentFunc? likeCommentFunc; - final Map? settings; + final Map>? settings; final Map>? translations; diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index c104d4a..4266193 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -923,8 +923,30 @@ class ComicSourceParser { }; } - Map _parseSettings() { - return _getValue("settings") ?? {}; + Map> _parseSettings() { + var value = _getValue("settings"); + if (value is Map) { + var newMap = >{}; + for (var e in value.entries) { + if (e.key is! String) { + continue; + } + var v = {}; + for (var e2 in e.value.entries) { + if (e2.key is! String) { + continue; + } + var v2 = e2.value; + if (v2 is JSInvokable) { + v2 = JSAutoFreeFunction(v2); + } + v[e2.key] = v2; + } + newMap[e.key] = v; + } + return newMap; + } + return {}; } RegExp? _parseIdMatch() { diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index f7e935a..1dcfca8 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -165,7 +165,7 @@ class JsEngine with _JSEngineApi { String settingKey = message["setting_key"]; var source = ComicSource.find(key)!; return source.data["settings"]?[settingKey] ?? - source.settings?[settingKey]['default'] ?? + source.settings?[settingKey]!['default'] ?? (throw "Setting not found: $settingKey"); } case "isLogged": @@ -688,3 +688,20 @@ class DocumentWrapper { return elements.length - 1; } } + +class JSAutoFreeFunction { + final JSInvokable func; + + /// Automatically free the function when it's not used anymore + JSAutoFreeFunction(this.func) { + finalizer.attach(this, func); + } + + dynamic call(List args) { + return func(args); + } + + static final finalizer = Finalizer((func) { + func.free(); + }); +} \ No newline at end of file diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 8c471a2..72063b3 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -244,6 +244,8 @@ class _BodyState extends State<_Body> { }, ), ); + } else if (type == "callback") { + yield _CallbackSetting(setting: item); } } catch (e, s) { Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); @@ -677,3 +679,48 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { ).fixHeight(32); } } + +class _CallbackSetting extends StatefulWidget { + const _CallbackSetting({required this.setting}); + + final MapEntry> setting; + + @override + State<_CallbackSetting> createState() => _CallbackSettingState(); +} + +class _CallbackSettingState extends State<_CallbackSetting> { + String get key => widget.setting.key; + + String get buttonText => widget.setting.value['buttonText'] ?? "Click"; + + String get title => widget.setting.value['title'] ?? key; + + bool isLoading = false; + + Future onClick() async { + var func = widget.setting.value['callback']; + var result = func([]); + if (result is Future) { + setState(() { + isLoading = true; + }); + await result; + setState(() { + isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title.ts(key)), + trailing: Button.normal( + onPressed: onClick, + isLoading: isLoading, + child: Text(buttonText.ts(key)), + ).fixHeight(32), + ); + } +}