mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Improve ui api
This commit is contained in:
@@ -1205,6 +1205,10 @@ class Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI related apis
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
let UI = {
|
let UI = {
|
||||||
/**
|
/**
|
||||||
* Show a message
|
* Show a message
|
||||||
@@ -1222,7 +1226,8 @@ let UI = {
|
|||||||
* Show a dialog. Any action will close the dialog.
|
* Show a dialog. Any action will close the dialog.
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @param content {string}
|
* @param content {string}
|
||||||
* @param actions {{text:string, callback: () => void}[]}
|
* @param actions {{text:string, callback: () => void | Promise<void>, 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) => {
|
showDialog: (title, content, actions) => {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
@@ -1245,4 +1250,31 @@ let UI = {
|
|||||||
url: url,
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
183
lib/components/js_ui.dart
Normal file
183
lib/components/js_ui.dart
Normal file
@@ -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<int, LoadingDialogController> _loadingDialogControllers = {};
|
||||||
|
|
||||||
|
dynamic 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());
|
||||||
|
}
|
||||||
|
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<String, dynamic> message) {
|
||||||
|
BuildContext? dialogContext;
|
||||||
|
var title = message['title'];
|
||||||
|
var content = message['content'];
|
||||||
|
var actions = <Widget>[];
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,6 @@ 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;
|
||||||
@@ -20,9 +19,8 @@ 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/components/js_ui.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';
|
||||||
@@ -42,7 +40,7 @@ class JavaScriptRuntimeException implements Exception {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsEngine with _JSEngineApi, _JsUiApi {
|
class JsEngine with _JSEngineApi, JsUiApi {
|
||||||
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
||||||
|
|
||||||
static JsEngine? _cache;
|
static JsEngine? _cache;
|
||||||
@@ -156,7 +154,7 @@ class JsEngine with _JSEngineApi, _JsUiApi {
|
|||||||
case "delay":
|
case "delay":
|
||||||
return Future.delayed(Duration(milliseconds: message["time"]));
|
return Future.delayed(Duration(milliseconds: message["time"]));
|
||||||
case "UI":
|
case "UI":
|
||||||
handleUIMessage(Map.from(message));
|
return handleUIMessage(Map.from(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -690,48 +688,3 @@ class JSAutoFreeFunction {
|
|||||||
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']) {
|
|
||||||
// [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(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user