Merge pull request #4301 from Kingtous/master
feat: add entry in left panel and dialog callback
This commit is contained in:
commit
c465c16f77
@ -557,7 +557,7 @@ void window_on_top(int? id) {
|
||||
}
|
||||
|
||||
typedef DialogBuilder = CustomAlertDialog Function(
|
||||
StateSetter setState, void Function([dynamic]) close);
|
||||
StateSetter setState, void Function([dynamic]) close, BuildContext context);
|
||||
|
||||
class Dialog<T> {
|
||||
OverlayEntry? entry;
|
||||
@ -660,7 +660,7 @@ class OverlayDialogManager {
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
return Listener(
|
||||
onPointerUp: (_) => innerClicked = true,
|
||||
child: builder(setState, close),
|
||||
child: builder(setState, close, overlayState.context),
|
||||
);
|
||||
})));
|
||||
});
|
||||
@ -680,7 +680,7 @@ class OverlayDialogManager {
|
||||
VoidCallback? onCancel}) {
|
||||
final tag = _tagCount.toString();
|
||||
_tagCount++;
|
||||
show((setState, close) {
|
||||
show((setState, close, context) {
|
||||
cancel() {
|
||||
dismissAll();
|
||||
if (onCancel != null) {
|
||||
@ -938,7 +938,7 @@ void msgBox(String id, String type, String title, String text, String link,
|
||||
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
|
||||
}
|
||||
dialogManager.show(
|
||||
(setState, close) => CustomAlertDialog(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(child: msgboxContent(type, title, text)),
|
||||
actions: buttons,
|
||||
@ -1011,7 +1011,7 @@ Widget msgboxContent(String type, String title, String text) {
|
||||
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
||||
Widget content, List<Widget> buttons,
|
||||
{bool hasCancel = true}) {
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
dialogManager.show((setState, close, context) => CustomAlertDialog(
|
||||
title: Text(
|
||||
translate(title),
|
||||
style: TextStyle(fontSize: 21),
|
||||
|
@ -224,7 +224,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
final style = TextStyle(fontSize: 14.0);
|
||||
String? errorMsg;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
@ -334,7 +334,7 @@ class _AddressBookState extends State<AddressBook> {
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController(text: field);
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
|
@ -65,7 +65,7 @@ void changeIdDialog() {
|
||||
RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
|
||||
];
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
debugPrint("onSubmit");
|
||||
newId = controller.text.trim();
|
||||
@ -175,7 +175,7 @@ void changeWhiteList({Function()? callback}) async {
|
||||
var controller = TextEditingController(text: newWhiteListField);
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("IP Whitelisting")),
|
||||
content: Column(
|
||||
@ -255,7 +255,7 @@ void changeWhiteList({Function()? callback}) async {
|
||||
Future<String> changeDirectAccessPort(
|
||||
String currentIP, String currentPort) async {
|
||||
final controller = TextEditingController(text: currentPort);
|
||||
await gFFI.dialogManager.show((setState, close) {
|
||||
await gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Change Local Port")),
|
||||
content: Column(
|
||||
@ -425,7 +425,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
|
||||
void wrongPasswordDialog(
|
||||
String id, OverlayDialogManager dialogManager, type, title, text) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
@ -498,7 +498,7 @@ _connectDialog(
|
||||
rememberAccount = await bind.sessionGetRemember(id: id) ?? false;
|
||||
}
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
cancel() {
|
||||
close();
|
||||
closeConnection();
|
||||
@ -653,7 +653,7 @@ void showWaitUacDialog(
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(
|
||||
tag: '$id-wait-uac',
|
||||
(setState, close) => CustomAlertDialog(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
|
||||
));
|
||||
@ -769,7 +769,7 @@ void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) {
|
||||
]));
|
||||
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show(tag: '$id-request-elevation', (setState, close) {
|
||||
dialogManager.show(tag: '$id-request-elevation', (setState, close, context) {
|
||||
void submit() {
|
||||
if (groupValue.value == 'logon') {
|
||||
if (userController.text.isEmpty) {
|
||||
@ -813,7 +813,7 @@ void showOnBlockDialog(
|
||||
dialogManager.existing('$id-request-elevation')) {
|
||||
return;
|
||||
}
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close, context) {
|
||||
void submit() {
|
||||
close();
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
@ -835,7 +835,7 @@ void showOnBlockDialog(
|
||||
|
||||
void showElevationError(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close, context) {
|
||||
void submit() {
|
||||
close();
|
||||
showRequestElevationDialog(id, dialogManager);
|
||||
@ -859,7 +859,7 @@ void showElevationError(String id, String type, String title, String text,
|
||||
void showWaitAcceptDialog(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
onCancel() {
|
||||
closeConnection();
|
||||
}
|
||||
@ -878,7 +878,7 @@ void showWaitAcceptDialog(String id, String type, String title, String text,
|
||||
void showRestartRemoteDevice(
|
||||
PeerInfo pi, String id, OverlayDialogManager dialogManager) async {
|
||||
final res =
|
||||
await dialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||
await dialogManager.show<bool>((setState, close, context) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
|
||||
Flexible(
|
||||
@ -915,7 +915,7 @@ showSetOSPassword(
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != '';
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: text);
|
||||
@ -983,7 +983,7 @@ showSetOSAccount(
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
usernameController.text = username;
|
||||
passwdController.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
final username = usernameController.text.trim();
|
||||
final password = usernameController.text.trim();
|
||||
@ -1115,7 +1115,7 @@ showAuditDialog(String id, dialogManager) async {
|
||||
|
||||
void showConfirmSwitchSidesDialog(
|
||||
String id, OverlayDialogManager dialogManager) async {
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
await bind.sessionSwitchSides(id: id);
|
||||
closeConnection(id: id);
|
||||
|
@ -391,7 +391,7 @@ Future<bool?> loginDialog() async {
|
||||
final autoLogin = true.obs;
|
||||
final RxString curOP = ''.obs;
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
username.addListener(() {
|
||||
if (usernameMsg != null) {
|
||||
setState(() => usernameMsg = null);
|
||||
@ -530,7 +530,7 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
|
||||
final focusNode = FocusNode()..requestFocus();
|
||||
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
bool validate() {
|
||||
return code.text.length >= 6;
|
||||
}
|
||||
|
@ -664,7 +664,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
RxBool isInProgress = false.obs;
|
||||
String name = await _getAlias(id);
|
||||
var controller = TextEditingController(text: name);
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
isInProgress.value = true;
|
||||
String name = controller.text.trim();
|
||||
@ -724,7 +724,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
void _delete(String id, bool isLan, Function reloadFunc) async {
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close) {
|
||||
(setState, close, context) {
|
||||
submit() async {
|
||||
if (isLan) {
|
||||
bind.mainRemoveDiscovered(id: id);
|
||||
@ -1023,7 +1023,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
final tags = List.of(gFFI.abModel.tags);
|
||||
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
@ -1115,7 +1115,7 @@ void _rdpDialog(String id) async {
|
||||
text: await bind.mainGetPeerOption(id: id, key: 'rdp_password'));
|
||||
RxBool secure = true.obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
String port = portController.text.trim();
|
||||
String username = userController.text;
|
||||
|
@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -88,6 +89,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
},
|
||||
),
|
||||
buildPluginEntry()
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -572,6 +574,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
_updateTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget buildPluginEntry() {
|
||||
final entries = PluginUiManager.instance.entries.entries;
|
||||
return Offstage(
|
||||
offstage: entries.isEmpty,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...
|
||||
entries.map((entry) {
|
||||
return entry.value;
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setPasswordDialog() async {
|
||||
@ -589,7 +607,7 @@ void setPasswordDialog() async {
|
||||
MinCharactersValidationRule(8),
|
||||
];
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
|
@ -2016,7 +2016,7 @@ void changeSocks5Proxy() async {
|
||||
RxBool obscure = true.obs;
|
||||
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
proxyMsg = '';
|
||||
|
@ -643,7 +643,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
MenuButton(
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
_ffi.dialogManager.show((setState, close) {
|
||||
_ffi.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
controller.createDir(PathUtil.join(
|
||||
|
@ -293,7 +293,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
),
|
||||
];
|
||||
gFFI.dialogManager.show(
|
||||
(setState, close) => CustomAlertDialog(
|
||||
(setState, close, context) => CustomAlertDialog(
|
||||
title: null,
|
||||
content: SelectionArea(
|
||||
child:
|
||||
|
@ -204,7 +204,7 @@ showKBLayoutTypeChooser(
|
||||
String localPlatform,
|
||||
OverlayDialogManager dialogManager,
|
||||
) {
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title:
|
||||
Text('${translate('Select local keyboard type')} ($localPlatform)'),
|
||||
|
@ -687,7 +687,7 @@ Future<bool> toggleMaximize(bool isMainWindow) async {
|
||||
|
||||
Future<bool> closeConfirmDialog() async {
|
||||
var confirm = true;
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
submit() {
|
||||
final opt = "enable-confirm-closing-tabs";
|
||||
String value = bool2option(opt, confirm);
|
||||
|
@ -197,7 +197,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
} else if (v == "folder") {
|
||||
final name = TextEditingController();
|
||||
gFFI.dialogManager
|
||||
.show((setState, close) => CustomAlertDialog(
|
||||
.show((setState, close, context) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -868,7 +868,7 @@ void showOptions(
|
||||
List<TToggleMenu> displayToggles =
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
var viewStyle =
|
||||
(viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
|
||||
var imageQuality =
|
||||
|
@ -318,7 +318,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
kRequestIgnoreBatteryOptimizations);
|
||||
} else {
|
||||
final res = await gFFI.dialogManager
|
||||
.show<bool>((setState, close) => CustomAlertDialog(
|
||||
.show<bool>((setState, close, context) => CustomAlertDialog(
|
||||
title: Text(translate("Open System Setting")),
|
||||
content: Text(translate(
|
||||
"android_open_battery_optimizations_tip")),
|
||||
@ -505,7 +505,7 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||
try {
|
||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||
var lang = bind.mainGetLocalOption(key: "lang");
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
setLang(v) {
|
||||
if (lang != v) {
|
||||
setState(() {
|
||||
@ -539,7 +539,7 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||
void showThemeSettings(OverlayDialogManager dialogManager) async {
|
||||
var themeMode = MyTheme.getThemeModePreference();
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
setTheme(v) {
|
||||
if (themeMode != v) {
|
||||
setState(() {
|
||||
@ -563,7 +563,7 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
||||
}
|
||||
|
||||
void showAbout(OverlayDialogManager dialogManager) {
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text('${translate('About')} RustDesk'),
|
||||
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
||||
|
@ -20,7 +20,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
|
||||
final p1 = TextEditingController(text: pw);
|
||||
var validateLength = false;
|
||||
var validateSame = false;
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
submit() async {
|
||||
close();
|
||||
dialogManager.showLoading(translate("Waiting"));
|
||||
@ -111,7 +111,7 @@ void setTemporaryPasswordLengthDialog(
|
||||
var index = lengths.indexOf(length);
|
||||
if (index < 0) index = 0;
|
||||
length = lengths[index];
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
setLength(newValue) {
|
||||
final oldValue = length;
|
||||
if (oldValue == newValue) return;
|
||||
@ -160,7 +160,7 @@ void showServerSettingsWithValue(
|
||||
String? relayServerMsg;
|
||||
String? apiServerMsg;
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
dialogManager.show((setState, close, context) {
|
||||
Future<bool> validate() async {
|
||||
if (idCtrl.text != oldCfg.idServer) {
|
||||
final res = await validateAsync(idCtrl.text);
|
||||
|
@ -152,7 +152,7 @@ class FileModel {
|
||||
String title, String content, bool showCheckbox, bool isIdentical) async {
|
||||
fileConfirmCheckboxRemember = false;
|
||||
return await parent.target?.dialogManager.show<bool?>(
|
||||
(setState, Function(bool? v) close) {
|
||||
(setState, Function(bool? v) close, context) {
|
||||
cancel() => close(false);
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
@ -547,7 +547,7 @@ class FileController {
|
||||
|
||||
Future<bool?> showRemoveDialog(
|
||||
String title, String content, bool showCheckbox) async {
|
||||
return await dialogManager?.show<bool>((setState, Function(bool v) close) {
|
||||
return await dialogManager?.show<bool>((setState, Function(bool v) close, context) {
|
||||
cancel() => close(false);
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
|
@ -383,7 +383,7 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
void showRelayHintDialog(String id, String type, String title, String text,
|
||||
OverlayDialogManager dialogManager) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close) {
|
||||
dialogManager.show(tag: '$id-$type', (setState, close, context) {
|
||||
onClose() {
|
||||
closeConnection();
|
||||
close();
|
||||
|
@ -289,7 +289,7 @@ class ServerModel with ChangeNotifier {
|
||||
toggleService() async {
|
||||
if (_isStart) {
|
||||
final res =
|
||||
await parent.target?.dialogManager.show<bool>((setState, close) {
|
||||
await parent.target?.dialogManager.show<bool>((setState, close, context) {
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
@ -312,7 +312,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
} else {
|
||||
final res =
|
||||
await parent.target?.dialogManager.show<bool>((setState, close) {
|
||||
await parent.target?.dialogManager.show<bool>((setState, close, context) {
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
@ -481,7 +481,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void showLoginDialog(Client client) {
|
||||
parent.target?.dialogManager.show((setState, close) {
|
||||
parent.target?.dialogManager.show((setState, close, context) {
|
||||
cancel() {
|
||||
sendLoginResponse(client, false);
|
||||
close();
|
||||
@ -699,7 +699,7 @@ String getLoginDialogTag(int id) {
|
||||
}
|
||||
|
||||
showInputWarnAlert(FFI ffi) {
|
||||
ffi.dialogManager.show((setState, close) {
|
||||
ffi.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
AndroidPermissionManager.startAction(kActionAccessibilitySettings);
|
||||
close();
|
||||
@ -726,7 +726,7 @@ showInputWarnAlert(FFI ffi) {
|
||||
}
|
||||
|
||||
Future<void> showClientsMayNotBeChangedAlert(FFI? ffi) async {
|
||||
await ffi?.dialogManager.show((setState, close) {
|
||||
await ffi?.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Permissions")),
|
||||
content: Column(
|
||||
|
@ -1,5 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/plugin/ui_manager.dart';
|
||||
import 'package:flutter_hbb/plugin/utils/dialogs.dart';
|
||||
|
||||
abstract class NativeHandler {
|
||||
bool onEvent(Map<String, dynamic> evt);
|
||||
}
|
||||
@ -30,6 +36,15 @@ class NativeUiHandler extends NativeHandler {
|
||||
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
|
||||
onSelectPeers(cbFuncDart, userData);
|
||||
break;
|
||||
case "register_ui_entry":
|
||||
int cb = evt['on_tap_cb'];
|
||||
int userData = evt['user_data'] ?? 0;
|
||||
String title = evt['title'] ?? "";
|
||||
final cbFuncNative = Pointer.fromAddress(cb)
|
||||
.cast<NativeFunction<OnSelectPeersCallback>>();
|
||||
final cbFuncDart = cbFuncNative.asFunction<OnSelectPeersCallbackDart>();
|
||||
onRegisterUiEntry(title, cbFuncDart, userData);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -37,7 +52,28 @@ class NativeUiHandler extends NativeHandler {
|
||||
}
|
||||
|
||||
void onSelectPeers(OnSelectPeersCallbackDart cb, int userData) async {
|
||||
// TODO: design a UI interface to pick peers.
|
||||
cb(0, Pointer.fromAddress(0), 0, Pointer.fromAddress(userData));
|
||||
showPeerSelectionDialog(onPeersCallback: (peers) {
|
||||
String json = jsonEncode(<String, dynamic> {
|
||||
"peers": peers
|
||||
});
|
||||
final native = json.toNativeUtf8();
|
||||
cb(0, native.cast(), native.length, Pointer.fromAddress(userData));
|
||||
malloc.free(native);
|
||||
});
|
||||
}
|
||||
|
||||
void onRegisterUiEntry(String title, OnSelectPeersCallbackDart cbFuncDart, int userData) {
|
||||
Widget widget = InkWell(
|
||||
child: Container(
|
||||
height: 25.0,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(title)),
|
||||
Icon(Icons.chevron_right_rounded, size: 12.0,)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
PluginUiManager.instance.registerEntry(title, widget);
|
||||
}
|
||||
}
|
||||
|
17
flutter/lib/plugin/ui_manager.dart
Normal file
17
flutter/lib/plugin/ui_manager.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PluginUiManager {
|
||||
PluginUiManager._();
|
||||
|
||||
static PluginUiManager instance = PluginUiManager._();
|
||||
|
||||
Map<String, Widget> entries = <String, Widget>{};
|
||||
|
||||
void registerEntry(String key, Widget widget) {
|
||||
entries[key] = widget;
|
||||
}
|
||||
|
||||
void unregisterEntry(String key) {
|
||||
entries.remove(key);
|
||||
}
|
||||
}
|
83
flutter/lib/plugin/utils/dialogs.dart
Normal file
83
flutter/lib/plugin/utils/dialogs.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
void showPeerSelectionDialog(
|
||||
{bool singleSelection = false,
|
||||
required Function(List<String>) onPeersCallback}) {
|
||||
final peers = bind.mainLoadRecentPeersSync();
|
||||
if (peers.isEmpty) {
|
||||
debugPrint("load recent peers sync failed.");
|
||||
return;
|
||||
}
|
||||
Map<String, dynamic> map = jsonDecode(peers);
|
||||
List<dynamic> peersList = map['peers'] ?? [];
|
||||
final selected = List<String>.empty(growable: true);
|
||||
|
||||
submit() async {
|
||||
onPeersCallback.call(selected);
|
||||
}
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
return CustomAlertDialog(
|
||||
title:
|
||||
Text(translate(singleSelection ? "Select peers" : "Select a peer")),
|
||||
content: SizedBox(
|
||||
height: 300.0,
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final Map<String, dynamic> peer = peersList[index];
|
||||
final String platform = peer['platform'] ?? "";
|
||||
final String id = peer['id'] ?? "";
|
||||
final String alias = peer['alias'] ?? "";
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selected.contains(id)) {
|
||||
selected.remove(id);
|
||||
} else {
|
||||
selected.add(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
key: ValueKey(index),
|
||||
height: 50.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).highlightColor,
|
||||
borderRadius: BorderRadius.circular(12.0)
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
margin: EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
// platform
|
||||
SizedBox(width: 8.0,),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
getPlatformImage(platform, size: 34.0),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 8.0,),
|
||||
// id/alias
|
||||
Expanded(child: Text(alias.isEmpty ? id : alias)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: peersList.length,
|
||||
itemExtent: 50.0,
|
||||
),
|
||||
),
|
||||
onSubmit: submit,
|
||||
);
|
||||
});
|
||||
}
|
@ -751,6 +751,25 @@ pub fn main_load_recent_peers() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_load_recent_peers_sync() -> SyncReturn<String> {
|
||||
if !config::APP_DIR.read().unwrap().is_empty() {
|
||||
let peers: Vec<HashMap<&str, String>> = PeerConfig::peers()
|
||||
.drain(..)
|
||||
.map(|(id, _, p)| peer_to_map(id, p))
|
||||
.collect();
|
||||
|
||||
let data = HashMap::from([
|
||||
("name", "load_recent_peers".to_owned()),
|
||||
(
|
||||
"peers",
|
||||
serde_json::ser::to_string(&peers).unwrap_or("".to_owned()),
|
||||
),
|
||||
]);
|
||||
return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
||||
}
|
||||
SyncReturn("".to_string())
|
||||
}
|
||||
|
||||
pub fn main_load_fav_peers() {
|
||||
if !config::APP_DIR.read().unwrap().is_empty() {
|
||||
let favs = get_fav();
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "指纹"),
|
||||
("Copy Fingerprint", "复制指纹"),
|
||||
("no fingerprints", "没有指纹"),
|
||||
("Select a peer", "选择一个被控端"),
|
||||
("Select peers", "选择被控"),
|
||||
("Plugins", "插件")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "Fingerabdruck"),
|
||||
("Copy Fingerprint", "Fingerabdruck kopieren"),
|
||||
("no fingerprints", "Keine Fingerabdrücke"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "Huella digital"),
|
||||
("Copy Fingerprint", "Copiar huella digital"),
|
||||
("no fingerprints", "sin huellas digitales"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "اثر انگشت"),
|
||||
("Copy Fingerprint", "کپی کردن اثر انگشت"),
|
||||
("no fingerprints", "بدون اثر انگشت"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "Firma digitale"),
|
||||
("Copy Fingerprint", "Copia firma digitale"),
|
||||
("no fingerprints", "Nessuna firma digitale"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
1007
src/lang/lt.rs
1007
src/lang/lt.rs
File diff suppressed because it is too large
Load Diff
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "Vingerafdruk"),
|
||||
("Copy Fingerprint", "Kopieer Vingerafdruk"),
|
||||
("no fingerprints", "geen vingerafdrukken"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "Sygnatura"),
|
||||
("Copy Fingerprint", "Skopiuj sygnaturę"),
|
||||
("no fingerprints", "brak sygnatur"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "Отпечаток"),
|
||||
("Copy Fingerprint", "Копировать отпечаток"),
|
||||
("no fingerprints", "отпечатки отсутствуют"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", "指紋"),
|
||||
("Copy Fingerprint", "複製指紋"),
|
||||
("no fingerprints", "沒有指紋"),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -498,5 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Fingerprint", ""),
|
||||
("Copy Fingerprint", ""),
|
||||
("no fingerprints", ""),
|
||||
("Select a peer", ""),
|
||||
("Select peers", ""),
|
||||
("Plugins", "")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -58,6 +58,33 @@ impl PluginNativeHandler for PluginNativeUIHandler {
|
||||
data: "missing cb field message".as_ptr() as _,
|
||||
});
|
||||
}
|
||||
"register_ui_entry" => {
|
||||
let title;
|
||||
if let Some(v) = data.get("title") {
|
||||
title = v.as_str().unwrap_or("");
|
||||
} else {
|
||||
title = "";
|
||||
}
|
||||
if let Some(on_tap_cb) = data.get("on_tap_cb") {
|
||||
if let Some(on_tap_cb) = on_tap_cb.as_u64() {
|
||||
let user_data = match data.get("user_data") {
|
||||
Some(user_data) => {
|
||||
user_data.as_u64().unwrap_or(0)
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
self.register_ui_entry(title, on_tap_cb, user_data);
|
||||
return Some(super::NR {
|
||||
return_type: 0,
|
||||
data: std::ptr::null(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return Some(super::NR {
|
||||
return_type: -1,
|
||||
data: "missing cb field message".as_ptr() as _,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
@ -97,4 +124,26 @@ impl PluginNativeUIHandler {
|
||||
serde_json::to_string(¶m).unwrap_or("".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Call with method `register_ui_entry` and the following json:
|
||||
/// ```
|
||||
/// {
|
||||
///
|
||||
/// "on_tap_cb": 0, // The function address
|
||||
/// "user_data": 0, // An opaque pointer value passed to the callback.
|
||||
/// "title": "entry name"
|
||||
/// }
|
||||
/// ```
|
||||
fn register_ui_entry(&self, title: &str, on_tap_cb: u64, user_data: u64) {
|
||||
let mut param = HashMap::new();
|
||||
param.insert("name", json!("native_ui"));
|
||||
param.insert("action", json!("register_ui_entry"));
|
||||
param.insert("title", json!(title));
|
||||
param.insert("cb", json!(on_tap_cb));
|
||||
param.insert("user_data", json!(user_data));
|
||||
crate::flutter::push_global_event(
|
||||
APP_TYPE_MAIN,
|
||||
serde_json::to_string(¶m).unwrap_or("".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user