Add UI api

This commit is contained in:
2025-01-18 16:53:05 +08:00
parent 1abf9c151e
commit dda8d98e85
3 changed files with 143 additions and 80 deletions

View File

@@ -498,6 +498,7 @@ let Network = {
* @param url {string} * @param url {string}
* @param options {{method: string, headers: Object, body: any}} * @param options {{method: string, headers: Object, body: any}}
* @returns {Promise<{ok: boolean, status: number, statusText: string, headers: {}, arrayBuffer: (function(): Promise<ArrayBuffer>), text: (function(): Promise<string>), json: (function(): Promise<any>)}>} * @returns {Promise<{ok: boolean, status: number, statusText: string, headers: {}, arrayBuffer: (function(): Promise<ArrayBuffer>), text: (function(): Promise<string>), json: (function(): Promise<any>)}>}
* @since 1.2.0
*/ */
async function fetch(url, options) { async function fetch(url, options) {
let method = 'GET'; let method = 'GET';
@@ -1203,3 +1204,45 @@ class Image {
return new Image(key); 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,
})
},
}

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:html/parser.dart' as html; import 'package:html/parser.dart' as html;
import 'package:html/dom.dart' as dom; 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/cfb.dart';
import 'package:pointycastle/block/modes/ecb.dart'; import 'package:pointycastle/block/modes/ecb.dart';
import 'package:pointycastle/block/modes/ofb.dart'; import 'package:pointycastle/block/modes/ofb.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/network/app_dio.dart'; import 'package:venera/network/app_dio.dart';
import 'package:venera/network/cookie_jar.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()); factory JsEngine() => _cache ?? (_cache = JsEngine._create());
static JsEngine? _cache; static JsEngine? _cache;
@@ -93,91 +96,67 @@ class JsEngine with _JSEngineApi {
String method = message["method"] as String; String method = message["method"] as String;
switch (method) { switch (method) {
case "log": case "log":
{ String level = message["level"];
String level = message["level"]; Log.addLog(
Log.addLog( switch (level) {
switch (level) { "error" => LogLevel.error,
"error" => LogLevel.error, "warning" => LogLevel.warning,
"warning" => LogLevel.warning, "info" => LogLevel.info,
"info" => LogLevel.info, _ => LogLevel.warning
_ => LogLevel.warning },
}, message["title"],
message["title"], message["content"].toString());
message["content"].toString());
}
case 'load_data': case 'load_data':
{ String key = message["key"];
String key = message["key"]; String dataKey = message["data_key"];
String dataKey = message["data_key"]; return ComicSource.find(key)?.data[dataKey];
return ComicSource.find(key)?.data[dataKey];
}
case 'save_data': case 'save_data':
{ String key = message["key"];
String key = message["key"]; String dataKey = message["data_key"];
String dataKey = message["data_key"]; if (dataKey == 'setting') {
if (dataKey == 'setting') { throw "setting is not allowed to be saved";
throw "setting is not allowed to be saved";
}
var data = message["data"];
var source = ComicSource.find(key)!;
source.data[dataKey] = data;
source.saveData();
} }
var data = message["data"];
var source = ComicSource.find(key)!;
source.data[dataKey] = data;
source.saveData();
case 'delete_data': case 'delete_data':
{ String key = message["key"];
String key = message["key"]; String dataKey = message["data_key"];
String dataKey = message["data_key"]; var source = ComicSource.find(key);
var source = ComicSource.find(key); source?.data.remove(dataKey);
source?.data.remove(dataKey); source?.saveData();
source?.saveData();
}
case 'http': case 'http':
{ return _http(Map.from(message));
return _http(Map.from(message));
}
case 'html': case 'html':
{ return handleHtmlCallback(Map.from(message));
return handleHtmlCallback(Map.from(message));
}
case 'convert': case 'convert':
{ return _convert(Map.from(message));
return _convert(Map.from(message));
}
case "random": case "random":
{ return _random(
return _random( message["min"] ?? 0,
message["min"] ?? 0, message["max"] ?? 1,
message["max"] ?? 1, message["type"],
message["type"], );
);
}
case "cookie": case "cookie":
{ return handleCookieCallback(Map.from(message));
return handleCookieCallback(Map.from(message));
}
case "uuid": case "uuid":
{ return const Uuid().v1();
return const Uuid().v1();
}
case "load_setting": case "load_setting":
{ String key = message["key"];
String key = message["key"]; String settingKey = message["setting_key"];
String settingKey = message["setting_key"]; var source = ComicSource.find(key)!;
var source = ComicSource.find(key)!; return source.data["settings"]?[settingKey] ??
return source.data["settings"]?[settingKey] ?? source.settings?[settingKey]!['default'] ??
source.settings?[settingKey]!['default'] ?? (throw "Setting not found: $settingKey");
(throw "Setting not found: $settingKey");
}
case "isLogged": case "isLogged":
{ return ComicSource.find(message["key"])!.isLogged;
return ComicSource.find(message["key"])!.isLogged; // temporary solution for [setTimeout] function
} // TODO: implement [setTimeout] in quickjs project
// temporary solution for [setTimeout] function
// TODO: implement [setTimeout] in quickjs project
case "delay": case "delay":
{ return Future.delayed(Duration(milliseconds: message["time"]));
return Future.delayed(Duration(milliseconds: message["time"])); case "UI":
} handleUIMessage(Map.from(message));
} }
} }
return null; return null;
@@ -710,4 +689,46 @@ class JSAutoFreeFunction {
static final finalizer = Finalizer<JSInvokable>((func) { static final finalizer = Finalizer<JSInvokable>((func) {
func.free(); func.free();
}); });
} }
mixin class _JsUiApi {
void handleUIMessage(Map<String, dynamic> 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<String, dynamic> message) {
var title = message['title'];
var content = message['content'];
var actions = <String, JSAutoFreeFunction>{};
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(),
);
});
}
}

View File

@@ -8,7 +8,6 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart'; import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/image.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -54,9 +53,6 @@ class _ComicSourcePageState extends State<ComicSourcePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: Appbar(
title: Text('Comic Source'.tl),
),
body: const _Body(), body: const _Body(),
); );
} }
@@ -92,6 +88,10 @@ class _BodyState extends State<_Body> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SmoothCustomScrollView( return SmoothCustomScrollView(
slivers: [ slivers: [
SliverAppbar(
title: Text('Comic Source'.tl),
style: AppbarStyle.shadow,
),
buildCard(context), buildCard(context),
for (var source in ComicSource.all()) buildSource(context, source), for (var source in ComicSource.all()) buildSource(context, source),
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
@@ -708,8 +708,7 @@ class _CallbackSettingState extends State<_CallbackSetting> {
}); });
try { try {
await result; await result;
} } finally {
finally {
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });