From dda8d98e85e5c36d82d8963c98ae1cf71acddda5 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 18 Jan 2025 16:53:05 +0800 Subject: [PATCH] Add UI api --- assets/init.js | 43 ++++++++ lib/foundation/js_engine.dart | 169 +++++++++++++++++-------------- lib/pages/comic_source_page.dart | 11 +- 3 files changed, 143 insertions(+), 80 deletions(-) diff --git a/assets/init.js b/assets/init.js index f87f487..5e7b234 100644 --- a/assets/init.js +++ b/assets/init.js @@ -498,6 +498,7 @@ let Network = { * @param url {string} * @param options {{method: string, headers: Object, body: any}} * @returns {Promise<{ok: boolean, status: number, statusText: string, headers: {}, arrayBuffer: (function(): Promise), text: (function(): Promise), json: (function(): Promise)}>} + * @since 1.2.0 */ async function fetch(url, options) { let method = 'GET'; @@ -1203,3 +1204,45 @@ class Image { return new Image(key); } } + +let UI = { + /** + * Show a message + * @param message {string} + */ + showMessage: (message) => { + sendMessage({ + method: 'UI', + function: 'showMessage', + message: message, + }) + }, + + /** + * Show a dialog. Any action will close the dialog. + * @param title {string} + * @param content {string} + * @param actions {{text:string, callback: () => void}[]} + */ + showDialog: (title, content, actions) => { + sendMessage({ + method: 'UI', + function: 'showDialog', + title: title, + content: content, + actions: actions, + }) + }, + + /** + * Open [url] in external browser + * @param url {string} + */ + launchUrl: (url) => { + sendMessage({ + method: 'UI', + function: 'launchUrl', + url: url, + }) + }, +} \ No newline at end of file diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index bc0d8a6..9accea8 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -3,6 +3,7 @@ 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; @@ -19,7 +20,9 @@ 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/foundation/app.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/network/cookie_jar.dart'; @@ -39,7 +42,7 @@ class JavaScriptRuntimeException implements Exception { } } -class JsEngine with _JSEngineApi { +class JsEngine with _JSEngineApi, _JsUiApi { factory JsEngine() => _cache ?? (_cache = JsEngine._create()); static JsEngine? _cache; @@ -93,91 +96,67 @@ class JsEngine with _JSEngineApi { String method = message["method"] as String; switch (method) { case "log": - { - String level = message["level"]; - Log.addLog( - switch (level) { - "error" => LogLevel.error, - "warning" => LogLevel.warning, - "info" => LogLevel.info, - _ => LogLevel.warning - }, - message["title"], - message["content"].toString()); - } + String level = message["level"]; + Log.addLog( + switch (level) { + "error" => LogLevel.error, + "warning" => LogLevel.warning, + "info" => LogLevel.info, + _ => LogLevel.warning + }, + message["title"], + message["content"].toString()); case 'load_data': - { - String key = message["key"]; - String dataKey = message["data_key"]; - return ComicSource.find(key)?.data[dataKey]; - } + String key = message["key"]; + String dataKey = message["data_key"]; + return ComicSource.find(key)?.data[dataKey]; case 'save_data': - { - String key = message["key"]; - String dataKey = message["data_key"]; - if (dataKey == 'setting') { - throw "setting is not allowed to be saved"; - } - var data = message["data"]; - var source = ComicSource.find(key)!; - source.data[dataKey] = data; - source.saveData(); + String key = message["key"]; + String dataKey = message["data_key"]; + if (dataKey == 'setting') { + throw "setting is not allowed to be saved"; } + var data = message["data"]; + var source = ComicSource.find(key)!; + source.data[dataKey] = data; + source.saveData(); case 'delete_data': - { - String key = message["key"]; - String dataKey = message["data_key"]; - var source = ComicSource.find(key); - source?.data.remove(dataKey); - source?.saveData(); - } + String key = message["key"]; + String dataKey = message["data_key"]; + var source = ComicSource.find(key); + source?.data.remove(dataKey); + source?.saveData(); case 'http': - { - return _http(Map.from(message)); - } + return _http(Map.from(message)); case 'html': - { - return handleHtmlCallback(Map.from(message)); - } + return handleHtmlCallback(Map.from(message)); case 'convert': - { - return _convert(Map.from(message)); - } + return _convert(Map.from(message)); case "random": - { - return _random( - message["min"] ?? 0, - message["max"] ?? 1, - message["type"], - ); - } + return _random( + message["min"] ?? 0, + message["max"] ?? 1, + message["type"], + ); case "cookie": - { - return handleCookieCallback(Map.from(message)); - } + return handleCookieCallback(Map.from(message)); case "uuid": - { - return const Uuid().v1(); - } + return const Uuid().v1(); case "load_setting": - { - String key = message["key"]; - String settingKey = message["setting_key"]; - var source = ComicSource.find(key)!; - return source.data["settings"]?[settingKey] ?? - source.settings?[settingKey]!['default'] ?? - (throw "Setting not found: $settingKey"); - } + String key = message["key"]; + String settingKey = message["setting_key"]; + var source = ComicSource.find(key)!; + return source.data["settings"]?[settingKey] ?? + source.settings?[settingKey]!['default'] ?? + (throw "Setting not found: $settingKey"); case "isLogged": - { - return ComicSource.find(message["key"])!.isLogged; - } - // temporary solution for [setTimeout] function - // TODO: implement [setTimeout] in quickjs project + return ComicSource.find(message["key"])!.isLogged; + // temporary solution for [setTimeout] function + // TODO: implement [setTimeout] in quickjs project case "delay": - { - return Future.delayed(Duration(milliseconds: message["time"])); - } + return Future.delayed(Duration(milliseconds: message["time"])); + case "UI": + handleUIMessage(Map.from(message)); } } return null; @@ -710,4 +689,46 @@ class JSAutoFreeFunction { static final finalizer = Finalizer((func) { func.free(); }); -} \ No newline at end of file +} + +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']) { + 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([]); + }, + child: Text(entry.key), + ); + }).toList(), + ); + }); + } +} diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index c642ec0..dd43ef1 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -8,7 +8,6 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/utils/ext.dart'; -import 'package:venera/utils/image.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; @@ -54,9 +53,6 @@ class _ComicSourcePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: Appbar( - title: Text('Comic Source'.tl), - ), body: const _Body(), ); } @@ -92,6 +88,10 @@ class _BodyState extends State<_Body> { Widget build(BuildContext context) { return SmoothCustomScrollView( slivers: [ + SliverAppbar( + title: Text('Comic Source'.tl), + style: AppbarStyle.shadow, + ), buildCard(context), for (var source in ComicSource.all()) buildSource(context, source), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), @@ -708,8 +708,7 @@ class _CallbackSettingState extends State<_CallbackSetting> { }); try { await result; - } - finally { + } finally { setState(() { isLoading = false; });