diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 16e8a172e..861b6b645 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -391,3 +391,10 @@ Future initGlobalFFI() async { // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); } + +String translate(String name) { + if (name.startsWith('Failed to') && name.contains(': ')) { + return name.split(': ').map((x) => translate(x)).join(': '); + } + return platformFFI.translate(name, localeName); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e32275373..d992c6c62 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -44,13 +44,22 @@ class _ConnectionPageState extends State { /// Update url. If it's not null, means an update is available. var _updateUrl = ''; - var _menuPos; Timer? _updateTimer; @override void initState() { super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.text) { + setState(() { + _idController.text = lastRemoteId; + }); + } + }(); + } _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { updateStatus(); }); @@ -58,7 +67,6 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { - if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( decoration: BoxDecoration(color: isDarkTheme() ? null : MyTheme.grayBg), child: Column( @@ -428,7 +436,7 @@ class _ConnectionPageState extends State { } updateStatus() async { - svcStopped.value = gFFI.getOption("stop-service") == "Y"; + svcStopped.value = bind.mainGetOption(key: "stop-service") == "Y"; final status = jsonDecode(await bind.mainGetConnectStatus()) as Map; svcStatusCode.value = status["status_num"]; @@ -444,7 +452,7 @@ class _ConnectionPageState extends State { } Future buildAddressBook(BuildContext context) async { - final token = await gFFI.getLocalOption('access_token'); + final token = await bind.mainGetLocalOption(key: 'access_token'); if (token.trim().isEmpty) { return Center( child: InkWell( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 6dc5e8f2f..3f908c3ce 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; @@ -156,6 +155,8 @@ class _DesktopHomePageState extends State with TrayListener { }, onTap: () async { final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); var menu = [ genEnablePopupMenuItem( translate("Enable Keyboard/Mouse"), @@ -173,7 +174,7 @@ class _DesktopHomePageState extends State with TrayListener { translate("Enable TCP Tunneling"), 'enable-tunnel', ), - genAudioInputPopupMenuItem(), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), PopupMenuDivider(), PopupMenuItem( child: Text(translate("ID/Relay Server")), @@ -465,49 +466,60 @@ class _DesktopHomePageState extends State with TrayListener { Get.find().setString("darkTheme", choice); } - void onSelectMenu(String value) { - if (value.startsWith('enable-')) { - final option = gFFI.getOption(value); - gFFI.setOption(value, option == "N" ? "" : "N"); - } else if (value.startsWith('allow-')) { - final option = gFFI.getOption(value); + void onSelectMenu(String key) async { + if (key.startsWith('enable-')) { + final option = await bind.mainGetOption(key: key); + bind.mainSetOption(key: key, value: option == "N" ? "" : "N"); + } else if (key.startsWith('allow-')) { + final option = await bind.mainGetOption(key: key); final choice = option == "Y" ? "" : "Y"; - gFFI.setOption(value, choice); + bind.mainSetOption(key: key, value: choice); changeTheme(choice); - } else if (value == "stop-service") { - final option = gFFI.getOption(value); - gFFI.setOption(value, option == "Y" ? "" : "Y"); - } else if (value == "change-id") { + } else if (key == "stop-service") { + final option = await bind.mainGetOption(key: key); + bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); + } else if (key == "change-id") { changeId(); - } else if (value == "custom-server") { + } else if (key == "custom-server") { changeServer(); - } else if (value == "whitelist") { + } else if (key == "whitelist") { changeWhiteList(); - } else if (value == "socks5-proxy") { + } else if (key == "socks5-proxy") { changeSocks5Proxy(); - } else if (value == "about") { + } else if (key == "about") { about(); - } else if (value == "logout") { + } else if (key == "logout") { logOut(); - } else if (value == "login") { + } else if (key == "login") { login(); } } - PopupMenuItem genEnablePopupMenuItem(String label, String value) { - final v = gFFI.getOption(value); - final isEnable = value.startsWith('enable-') ? v != "N" : v == "Y"; + PopupMenuItem genEnablePopupMenuItem(String label, String key) { + Future getOptionEnable(String key) async { + final v = await bind.mainGetOption(key: key); + return key.startsWith('enable-') ? v != "N" : v == "Y"; + } + return PopupMenuItem( - child: Row( - children: [ - Offstage(offstage: !isEnable, child: Icon(Icons.check)), - Text( - label, - style: genTextStyle(isEnable), - ), - ], - ), - value: value, + child: FutureBuilder( + future: getOptionEnable(key), + builder: (context, snapshot) { + var enable = false; + if (snapshot.hasData && snapshot.data!) { + enable = true; + } + return Row( + children: [ + Offstage(offstage: !enable, child: Icon(Icons.check)), + Text( + label, + style: genTextStyle(enable), + ), + ], + ); + }), + value: key, ); } @@ -518,10 +530,11 @@ class _DesktopHomePageState extends State with TrayListener { color: Colors.redAccent, decoration: TextDecoration.lineThrough); } - PopupMenuItem genAudioInputPopupMenuItem() { - final _enabledInput = gFFI.getOption('enable-audio'); - var defaultInput = gFFI.getDefaultAudioInput().obs; - var enabled = (_enabledInput != "N").obs; + PopupMenuItem genAudioInputPopupMenuItem( + bool enableInput, String defaultAudioInput) { + final defaultInput = defaultAudioInput.obs; + final enabled = enableInput.obs; + return PopupMenuItem( child: FutureBuilder>( future: gFFI.getAudioInputs(), @@ -569,12 +582,13 @@ class _DesktopHomePageState extends State with TrayListener { alignment: Alignment.centerLeft, child: Text(translate("Audio Input"))), itemBuilder: (context) => inputList, - onSelected: (dev) { + onSelected: (dev) async { if (dev == "Mute") { - gFFI.setOption( - 'enable-audio', _enabledInput == 'N' ? '' : 'N'); - enabled.value = gFFI.getOption('enable-audio') != 'N'; - } else if (dev != gFFI.getDefaultAudioInput()) { + await bind.mainSetOption( + key: 'enable-audio', value: enabled.value ? '' : 'N'); + enabled.value = + await bind.mainGetOption(key: 'enable-audio') != 'N'; + } else if (dev != await gFFI.getDefaultAudioInput()) { gFFI.setDefaultAudioInput(dev); defaultInput.value = dev; } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 0782e8426..949c46234 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -89,7 +89,8 @@ class _PeerCardState extends State<_PeerCard> children: [ Expanded( child: FutureBuilder( - future: gFFI.getPeerOption(peer.id, 'alias'), + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), builder: (_, snapshot) { if (snapshot.hasData) { final name = snapshot.data!.isEmpty @@ -304,7 +305,7 @@ class _PeerCardState extends State<_PeerCard> void _rename(String id) async { var isInProgress = false; - var name = await gFFI.getPeerOption(id, 'alias'); + var name = await bind.mainGetPeerOption(id: id, key: 'alias'); if (widget.type == PeerType.ab) { final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); if (peer == null) { @@ -359,7 +360,8 @@ class _PeerCardState extends State<_PeerCard> if (k.currentState != null) { if (k.currentState!.validate()) { k.currentState!.save(); - await gFFI.setPeerOption(id, 'alias', name); + await bind.mainSetPeerOption( + id: id, key: 'alias', value: name); if (widget.type == PeerType.ab) { gFFI.abModel.setPeerOption(id, 'alias', name); await gFFI.abModel.updateAb(); diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 9722b1a47..69e7b9433 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,6 +7,8 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../models/model.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; import 'home_page.dart'; import 'remote_page.dart'; import 'scan_page.dart'; @@ -41,6 +43,16 @@ class _ConnectionPageState extends State { @override void initState() { super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.text) { + setState(() { + _idController.text = lastRemoteId; + }); + } + }(); + } if (isAndroid) { Timer(Duration(seconds: 5), () { _updateUrl = gFFI.getByName('software_update_url'); @@ -52,7 +64,6 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -221,44 +232,52 @@ class _ConnectionPageState extends State { final n = (windowWidth / (minWidth + 2 * space)).floor(); width = windowWidth / n - 2 * space; } - final cards = []; - var peers = gFFI.peers(); - peers.forEach((p) { - cards.add(Container( - width: width, - child: Card( - child: GestureDetector( - onTap: !isWebDesktop ? () => connect('${p.id}') : null, - onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: ListTile( - contentPadding: const EdgeInsets.only(left: 12), - subtitle: Text('${p.username}@${p.hostname}'), - title: Text('${p.id}'), - leading: Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'), - color: str2color('${p.id}${p.platform}', 0x7f)), - trailing: InkWell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), - ))))); - }); - return Wrap(children: cards, spacing: space, runSpacing: space); + return FutureBuilder>( + future: gFFI.peers(), + builder: (context, snapshot) { + final cards = []; + if (snapshot.hasData) { + final peers = snapshot.data!; + peers.forEach((p) { + cards.add(Container( + width: width, + child: Card( + child: GestureDetector( + onTap: + !isWebDesktop ? () => connect('${p.id}') : null, + onDoubleTap: + isWebDesktop ? () => connect('${p.id}') : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + showPeerMenu(context, p.id); + }, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${p.username}@${p.hostname}'), + title: Text('${p.id}'), + leading: Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage('${p.platform}'), + color: str2color('${p.id}${p.platform}', 0x7f)), + trailing: InkWell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id); + }), + ))))); + }); + } + return Wrap(children: cards, spacing: space, runSpacing: space); + }); } /// Show the peer menu and handle user's choice. diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 2f5a9d991..54ba44892 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -9,7 +9,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:zxing2/qrcode.dart'; import '../../common.dart'; -import '../../models/model.dart'; +import '../../models/platform_model.dart'; class ScanPage extends StatefulWidget { @override @@ -153,54 +153,80 @@ class _ScanPageState extends State { } void showServerSettingsWithValue( - String id, String relay, String key, String api) { - final formKey = GlobalKey(); - final id0 = gFFI.getByName('option', 'custom-rendezvous-server'); - final relay0 = gFFI.getByName('option', 'relay-server'); - final api0 = gFFI.getByName('option', 'api-server'); - final key0 = gFFI.getByName('option', 'key'); + String id, String relay, String key, String api) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + String id0 = oldOptions['custom-rendezvous-server'] ?? ""; + String relay0 = oldOptions['relay-server'] ?? ""; + String api0 = oldOptions['api-server'] ?? ""; + String key0 = oldOptions['key'] ?? ""; + var isInProgress = false; + final idController = TextEditingController(text: id); + final relayController = TextEditingController(text: relay); + final apiController = TextEditingController(text: api); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + DialogManager.show((setState, close) { + Future validate() async { + if (idController.text != id) { + final res = await validateAsync(idController.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + id = idController.text; + } + if (relayController.text != relay) { + relayServerMsg = await validateAsync(relayController.text); + if (relayServerMsg != null) return false; + relay = relayController.text; + } + if (apiController.text != relay) { + apiServerMsg = await validateAsync(apiController.text); + if (apiServerMsg != null) return false; + api = apiController.text; + } + return true; + } + return CustomAlertDialog( title: Text(translate('ID/Relay Server')), content: Form( - key: formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( - initialValue: id, + controller: idController, decoration: InputDecoration( - labelText: translate('ID Server'), - ), - validator: validate, - onSaved: (String? value) { - if (value != null) id = value.trim(); - }, + labelText: translate('ID Server'), + errorText: idServerMsg), ) ] + (isAndroid ? [ TextFormField( - initialValue: relay, + controller: relayController, decoration: InputDecoration( - labelText: translate('Relay Server'), - ), - validator: validate, - onSaved: (String? value) { - if (value != null) relay = value.trim(); - }, + labelText: translate('Relay Server'), + errorText: relayServerMsg), ) ] : []) + [ TextFormField( - initialValue: api, + controller: apiController, decoration: InputDecoration( labelText: translate('API Server'), ), - validator: validate, - onSaved: (String? value) { - if (value != null) api = value.trim(); + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.length > 0) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; }, ), TextFormField( @@ -208,11 +234,13 @@ void showServerSettingsWithValue( decoration: InputDecoration( labelText: 'Key', ), - validator: null, - onSaved: (String? value) { + onChanged: (String? value) { if (value != null) key = value.trim(); }, ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) ])), actions: [ TextButton( @@ -224,24 +252,28 @@ void showServerSettingsWithValue( ), TextButton( style: flatButtonStyle, - onPressed: () { - if (formKey.currentState != null && - formKey.currentState!.validate()) { - formKey.currentState!.save(); - if (id != id0) - gFFI.setByName('option', - '{"name": "custom-rendezvous-server", "value": "$id"}'); + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (id != id0) { + bind.mainSetOption(key: "custom-rendezvous-server", value: id); + } if (relay != relay0) - gFFI.setByName( - 'option', '{"name": "relay-server", "value": "$relay"}'); - if (key != key0) - gFFI.setByName('option', '{"name": "key", "value": "$key"}'); + bind.mainSetOption(key: "relay-server", value: relay); + if (key != key0) bind.mainSetOption(key: "key", value: key); if (api != api0) - gFFI.setByName( - 'option', '{"name": "api-server", "value": "$api"}'); + bind.mainSetOption(key: "api-server", value: api); gFFI.ffiModel.updateUser(); close(); } + setState(() { + isInProgress = false; + }); }, child: Text(translate('OK')), ), @@ -250,11 +282,11 @@ void showServerSettingsWithValue( }); } -String? validate(value) { +Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { return null; } - final res = gFFI.getByName('test_if_valid_server', value); + final res = await bind.mainTestIfValidServer(server: value); return res.isEmpty ? null : res; } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 01cf4ae5d..3646b59e9 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -185,11 +185,12 @@ class _SettingsState extends State with WidgetsBindingObserver { } } -void showServerSettings() { - final id = gFFI.getByName('option', 'custom-rendezvous-server'); - final relay = gFFI.getByName('option', 'relay-server'); - final api = gFFI.getByName('option', 'api-server'); - final key = gFFI.getByName('option', 'key'); +void showServerSettings() async { + Map options = jsonDecode(await bind.mainGetOptions()); + String id = options['custom-rendezvous-server'] ?? ""; + String relay = options['relay-server'] ?? ""; + String api = options['api-server'] ?? ""; + String key = options['key'] ?? ""; showServerSettingsWithValue(id, relay, key, api); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index ad572c164..28ffa65e2 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import '../../mobile/widgets/overlay.dart'; import 'model.dart'; @@ -72,7 +73,7 @@ class ChatModel with ChangeNotifier { } } - receive(int id, String text) { + receive(int id, String text) async { if (text.isEmpty) return; // first message show overlay icon if (chatIconOverlayEntry == null) { @@ -82,7 +83,7 @@ class ChatModel with ChangeNotifier { if (id == clientModeID) { chatUser = ChatUser( firstName: _ffi.target?.ffiModel.pi.username, - id: _ffi.target?.getId() ?? "", + id: await bind.mainGetLastRemoteId(), ); } else { final client = _ffi.target?.serverModel.clients[id]; diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 459e8c448..45f5ec970 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -444,7 +444,7 @@ class FileModel extends ChangeNotifier { items.items.forEach((from) async { _jobId++; await bind.sessionSendFiles( - id: '${_ffi.target?.getId()}', + id: await bind.mainGetLastRemoteId(), actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f67d0d5fa..7ca77f6cd 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -881,11 +881,6 @@ class FFI { this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); } - /// Get the remote id for current client. - String getId() { - return getByName('remote_id'); // TODO - } - /// Send a mouse tap event(down and up). void tap(MouseButtons button) { sendMouse('down', button); @@ -963,9 +958,9 @@ class FFI { } /// List the saved peers. - List peers() { + Future> peers() async { try { - var str = getByName('peers'); // TODO + var str = await bind.mainGetRecentPeers(); if (str == "") return []; List peers = json.decode(str); return peers @@ -1046,33 +1041,6 @@ class FFI { platformFFI.setByName(name, value); } - String getOption(String name) { - return platformFFI.getByName("option", name); - } - - Future getLocalOption(String name) { - return bind.mainGetLocalOption(key: name); - } - - Future setLocalOption(String key, String value) { - return bind.mainSetLocalOption(key: key, value: value); - } - - Future getPeerOption(String id, String key) { - return bind.mainGetPeerOption(id: id, key: key); - } - - Future setPeerOption(String id, String key, String value) { - return bind.mainSetPeerOption(id: id, key: key, value: value); - } - - void setOption(String name, String value) { - Map res = Map() - ..["name"] = name - ..["value"] = value; - return platformFFI.setByName('option', jsonEncode(res)); - } - handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; var isMove = false; @@ -1148,8 +1116,8 @@ class FFI { return await bind.mainGetSoundInputs(); } - String getDefaultAudioInput() { - final input = getOption('audio-input'); + Future getDefaultAudioInput() async { + final input = await bind.mainGetOption(key: 'audio-input'); if (input.isEmpty && Platform.isWindows) { return "System Sound"; } @@ -1157,11 +1125,14 @@ class FFI { } void setDefaultAudioInput(String input) { - setOption('audio-input', input); + bind.mainSetOption(key: 'audio-input', value: input); } Future> getHttpHeaders() async { - return {"Authorization": "Bearer " + await getLocalOption("access_token")}; + return { + "Authorization": + "Bearer " + await bind.mainGetLocalOption(key: "access_token") + }; } } @@ -1233,11 +1204,12 @@ void initializeCursorAndCanvas(FFI ffi) async { /// Translate text based on the pre-defined dictionary. /// note: params [FFI?] can be used to replace global FFI implementation /// for example: during global initialization, gFFI not exists yet. -String translate(String name, {FFI? ffi}) { - if (name.startsWith('Failed to') && name.contains(': ')) { - return name.split(': ').map((x) => translate(x)).join(': '); - } - var a = 'translate'; - var b = '{"locale": "$localeName", "text": "$name"}'; - return (ffi ?? gFFI).getByName(a, b); -} +// String translate(String name, {FFI? ffi}) { +// if (name.startsWith('Failed to') && name.contains(': ')) { +// return name.split(': ').map((x) => translate(x)).join(': '); +// } +// var a = 'translate'; +// var b = '{"locale": "$localeName", "text": "$name"}'; +// +// return (ffi ?? gFFI).getByName(a, b); +// } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 784ffe6c8..c58577945 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -29,6 +29,7 @@ typedef HandleEvent = void Function(Map evt); class PlatformFFI { String _dir = ''; String _homeDir = ''; + F2? _translate; F2? _getByName; F3? _setByName; var _eventHandlers = Map>(); @@ -75,6 +76,19 @@ class PlatformFFI { } } + String translate(String name, String locale) { + if (_translate == null) return ''; + var a = name.toNativeUtf8(); + var b = locale.toNativeUtf8(); + var p = _translate!(a, b); + assert(p != nullptr); + final res = p.toDartString(); + calloc.free(p); + calloc.free(a); + calloc.free(b); + return res; + } + /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. String getByName(String name, [String arg = '']) { @@ -118,6 +132,7 @@ class PlatformFFI { : DynamicLibrary.process(); debugPrint('initializing FFI ${_appType}'); try { + _translate = dylib.lookupFunction('translate'); _getByName = dylib.lookupFunction('get_by_name'); _setByName = dylib.lookupFunction, Pointer), F3>( diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index c9147441e..362e47a78 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:wakelock/wakelock.dart'; import '../common.dart'; @@ -57,7 +58,7 @@ class ServerModel with ChangeNotifier { set verificationMethod(String method) { _verificationMethod = method; - gFFI.setOption("verification-method", method); + bind.mainSetOption(key: "verification-method", value: method); } String get temporaryPasswordLength { @@ -70,7 +71,7 @@ class ServerModel with ChangeNotifier { set temporaryPasswordLength(String length) { _temporaryPasswordLength = length; - gFFI.setOption("temporary-password-length", length); + bind.mainSetOption(key: "temporary-password-length", value: length); } TextEditingController get serverId => _serverId; @@ -85,7 +86,7 @@ class ServerModel with ChangeNotifier { ServerModel(this.parent) { () async { - _emptyIdShow = translate("Generating ...", ffi: this.parent.target); + _emptyIdShow = translate("Generating ..."); _serverId = TextEditingController(text: this._emptyIdShow); /** * 1. check android permission @@ -153,11 +154,13 @@ class ServerModel with ChangeNotifier { }); } - updatePasswordModel() { + updatePasswordModel() async { var update = false; final temporaryPassword = gFFI.getByName("temporary_password"); - final verificationMethod = gFFI.getOption("verification-method"); - final temporaryPasswordLength = gFFI.getOption("temporary-password-length"); + final verificationMethod = + await bind.mainGetOption(key: "verification-method"); + final temporaryPasswordLength = + await bind.mainGetOption(key: "temporary-password-length"); final oldPwdText = _serverPasswd.text; if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; @@ -325,7 +328,7 @@ class ServerModel with ChangeNotifier { const maxCount = 10; while (count < maxCount) { await Future.delayed(Duration(seconds: 1)); - final id = parent.target?.getByName("server_id") ?? ""; + final id = await bind.mainGetMyId(); if (id.isEmpty) { continue; } else { diff --git a/flutter/lib/utils/tray_manager.dart b/flutter/lib/utils/tray_manager.dart index d911932e5..f0422f554 100644 --- a/flutter/lib/utils/tray_manager.dart +++ b/flutter/lib/utils/tray_manager.dart @@ -1,8 +1,9 @@ import 'dart:io'; -import 'package:flutter_hbb/models/model.dart'; import 'package:tray_manager/tray_manager.dart'; +import '../common.dart'; + Future initTray({List? extra_item}) async { List items = [ MenuItem(key: "show", label: translate("show rustdesk")), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 904912715..40f72444a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -23,10 +23,10 @@ use crate::ui_interface; use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ discover, forget_password, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, - get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, - has_rendezvous_service, post_request, set_local_option, set_options, set_peer_option, - set_socks, store_fav, test_if_valid_server, using_public_server, + get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_option, + get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, + has_rendezvous_service, post_request, set_local_option, set_option, set_options, + set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -81,14 +81,24 @@ pub enum EventToUI { } pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { - if let Some(_) = flutter::GLOBAL_EVENT_STREAM.write().unwrap().insert(app_type.clone(), s) { - log::warn!("Global event stream of type {} is started before, but now removed", app_type); + if let Some(_) = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .insert(app_type.clone(), s) + { + log::warn!( + "Global event stream of type {} is started before, but now removed", + app_type + ); } Ok(()) } pub fn stop_global_event_stream(app_type: String) { - let _ = flutter::GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type); + let _ = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .remove(&app_type); } pub fn host_stop_system_key_propagate(stopped: bool) { @@ -113,7 +123,6 @@ pub fn get_session_remember(id: String) -> Option { } } -// TODO sync pub fn get_session_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(&arg)) @@ -143,7 +152,6 @@ pub fn get_session_option(id: String, arg: String) -> Option { } } -// void pub fn session_login(id: String, password: String, remember: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.login(&password, remember); @@ -409,6 +417,26 @@ pub fn main_get_async_status() -> String { get_async_job_status() } +pub fn main_get_option(key: String) -> String { + get_option(key) +} + +pub fn main_set_option(key: String, value: String) { + if key.eq("custom-rendezvous-server") { + set_option(key, value); + #[cfg(target_os = "android")] + crate::rendezvous_mediator::RendezvousMediator::restart(); + #[cfg(any( + target_os = "android", + target_os = "ios", + feature = "cli" + ))] + crate::common::test_rendezvous_server(); + } else { + set_option(key, value); + } +} + pub fn main_get_options() -> String { get_options() } @@ -452,7 +480,7 @@ pub fn main_store_fav(favs: Vec) { store_fav(favs) } -pub fn main_get_peers(id: String) -> String { +pub fn main_get_peer(id: String) -> String { let conf = get_peer(id); serde_json::to_string(&conf).unwrap_or("".to_string()) } @@ -525,13 +553,30 @@ pub fn main_forget_password(id: String) { forget_password(id) } +// TODO APP_DIR & ui_interface +pub fn main_get_recent_peers() -> String { + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .drain(..) + .map(|(id, _, p)| (id, p.info)) + .collect(); + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()) + } else { + String::new() + } +} + pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() .drain(..) .map(|(id, _, p)| (id, p.info)) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "load_recent_peers".to_owned()), ( @@ -557,7 +602,11 @@ pub fn main_load_fav_peers() { } }) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "load_fav_peers".to_owned()), ( @@ -571,7 +620,11 @@ pub fn main_load_fav_peers() { } pub fn main_load_lan_peers() { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "load_lan_peers".to_owned()), ("peers", get_lan_peers()), @@ -580,6 +633,25 @@ pub fn main_load_lan_peers() { }; } +pub fn main_get_last_remote_id() -> String { + // if !config::APP_DIR.read().unwrap().is_empty() { + // res = LocalConfig::get_remote_id(); + // } + LocalConfig::get_remote_id() +} + +#[no_mangle] +unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { + let name = CStr::from_ptr(name); + let locale = CStr::from_ptr(locale); + let res = if let (Ok(name), Ok(locale)) = (name.to_str(), locale.to_str()) { + crate::client::translate_locale(name.to_owned(), locale) + } else { + String::new() + }; + CString::from_vec_unchecked(res.into_bytes()).into_raw() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// @@ -594,93 +666,41 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co let name: &CStr = CStr::from_ptr(name); if let Ok(name) = name.to_str() { match name { - "peers" => { - if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() - .drain(..) - .map(|(id, _, p)| (id, p.info)) - .collect(); - res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); - } - } - "remote_id" => { - if !config::APP_DIR.read().unwrap().is_empty() { - res = LocalConfig::get_remote_id(); - } - } - // "remember" => { - // res = Session::get_remember().to_string(); - // } - // "toggle_option" => { - // if let Ok(arg) = arg.to_str() { - // if let Some(v) = Session::get_toggle_option(arg) { - // res = v.to_string(); - // } + // "peers" => { + // if !config::APP_DIR.read().unwrap().is_empty() { + // let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + // .drain(..) + // .map(|(id, _, p)| (id, p.info)) + // .collect(); + // res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); // } // } - "test_if_valid_server" => { - if let Ok(arg) = arg.to_str() { - res = hbb_common::socket_client::test_if_valid_server(arg); - } - } - "option" => { - if let Ok(arg) = arg.to_str() { - res = ui_interface::get_option(arg.to_owned()); - } - } - // "image_quality" => { - // res = Session::get_image_quality(); + // "remote_id" => { + // if !config::APP_DIR.read().unwrap().is_empty() { + // res = LocalConfig::get_remote_id(); + // } + // } + // "test_if_valid_server" => { + // if let Ok(arg) = arg.to_str() { + // res = hbb_common::socket_client::test_if_valid_server(arg); + // } + // } + // "option" => { + // if let Ok(arg) = arg.to_str() { + // res = ui_interface::get_option(arg.to_owned()); + // } // } "software_update_url" => { res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } - "translate" => { - if let Ok(arg) = arg.to_str() { - if let Ok(m) = serde_json::from_str::>(arg) { - if let Some(locale) = m.get("locale") { - if let Some(text) = m.get("text") { - res = crate::client::translate_locale(text.to_owned(), locale); - } - } - } - } - } - // "peer_option" => { - // if let Ok(arg) = arg.to_str() { - // res = Session::get_option(arg); - // } - // } // File Action "get_home_dir" => { res = fs::get_home_as_string(); } - // "read_local_dir_sync" => { - // if let Ok(value) = arg.to_str() { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(path), Some(show_hidden)) = - // (m.get("path"), m.get("show_hidden")) - // { - // if let Ok(fd) = - // fs::read_dir(&fs::get_path(path), show_hidden.eq("true")) - // { - // res = make_fd_to_json(fd); - // } - // } - // } - // } - // } // Server Side - "local_option" => { - if let Ok(arg) = arg.to_str() { - res = LocalConfig::get_option(arg); - } - } "langs" => { res = crate::lang::LANGS.to_string(); } - "server_id" => { - res = ui_interface::get_id(); - } "temporary_password" => { res = ui_interface::temporary_password(); } @@ -709,9 +729,6 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } } - "uuid" => { - res = base64::encode(get_uuid()); - } _ => { log::error!("Unknown name of get_by_name: {}", name); } @@ -742,69 +759,9 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { "info2" => { *crate::common::FLUTTER_INFO2.lock().unwrap() = value.to_owned(); } - // "connect" => { - // Session::start(value, false); - // } - // "connect_file_transfer" => { - // Session::start(value, true); - // } - // "login" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let Some(password) = m.get("password") { - // if let Some(remember) = m.get("remember") { - // Session::login(password, remember == "true"); - // } - // } - // } - // } - // "close" => { - // Session::close(); - // } - // "refresh" => { - // Session::refresh(); - // } - // "reconnect" => { - // Session::reconnect(); - // } - // "toggle_option" => { - // Session::toggle_option(value); - // } - // "image_quality" => { - // Session::set_image_quality(value); - // } - // "lock_screen" => { - // Session::lock_screen(); - // } - // "ctrl_alt_del" => { - // Session::ctrl_alt_del(); - // } - // "switch_display" => { - // if let Ok(v) = value.parse::() { - // Session::switch_display(v); - // } - // } "remove" => { PeerConfig::remove(value); } - // "input_key" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // let alt = m.get("alt").is_some(); - // let ctrl = m.get("ctrl").is_some(); - // let shift = m.get("shift").is_some(); - // let command = m.get("command").is_some(); - // let down = m.get("down").is_some(); - // let press = m.get("press").is_some(); - // if let Some(name) = m.get("name") { - // Session::input_key(name, down, press, alt, ctrl, shift, command); - // } - // } - // } - // "input_string" => { - // Session::input_string(value); - // } - // "chat_client_mode" => { - // Session::send_chat(value.to_owned()); - // } // TODO "send_mouse" => { @@ -848,203 +805,29 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } } } - "option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - ui_interface::set_option(name.to_owned(), value.to_owned()); - if name == "custom-rendezvous-server" { - #[cfg(target_os = "android")] - crate::rendezvous_mediator::RendezvousMediator::restart(); - #[cfg(any( - target_os = "android", - target_os = "ios", - feature = "cli" - ))] - crate::common::test_rendezvous_server(); - } - } - } - } - } - // "peer_option" => { + // "option" => { // if let Ok(m) = serde_json::from_str::>(value) { // if let Some(name) = m.get("name") { // if let Some(value) = m.get("value") { - // Session::set_option(name.to_owned(), value.to_owned()); + // ui_interface::set_option(name.to_owned(), value.to_owned()); + // if name == "custom-rendezvous-server" { + // #[cfg(target_os = "android")] + // crate::rendezvous_mediator::RendezvousMediator::restart(); + // #[cfg(any( + // target_os = "android", + // target_os = "ios", + // feature = "cli" + // ))] + // crate::common::test_rendezvous_server(); + // } // } // } // } // } - "local_option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - LocalConfig::set_option(name.to_owned(), value.to_owned()); - } - } - } - } - // "input_os_password" => { - // Session::input_os_password(value.to_owned(), true); - // } "restart_remote_device" => { // TODO // Session::restart_remote_device(); } - // // File Action - // "read_remote_dir" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(path), Some(show_hidden), Some(session)) = ( - // m.get("path"), - // m.get("show_hidden"), - // Session::get().read().unwrap().as_ref(), - // ) { - // session.read_remote_dir(path.to_owned(), show_hidden.eq("true")); - // } - // } - // } - // "send_files" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let ( - // Some(id), - // Some(path), - // Some(to), - // Some(file_num), - // Some(show_hidden), - // Some(is_remote), - // ) = ( - // m.get("id"), - // m.get("path"), - // m.get("to"), - // m.get("file_num"), - // m.get("show_hidden"), - // m.get("is_remote"), - // ) { - // Session::send_files( - // id.parse().unwrap_or(0), - // path.to_owned(), - // to.to_owned(), - // file_num.parse().unwrap_or(0), - // show_hidden.eq("true"), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "set_confirm_override_file" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let ( - // Some(id), - // Some(file_num), - // Some(need_override), - // Some(remember), - // Some(is_upload), - // ) = ( - // m.get("id"), - // m.get("file_num"), - // m.get("need_override"), - // m.get("remember"), - // m.get("is_upload"), - // ) { - // Session::set_confirm_override_file( - // id.parse().unwrap_or(0), - // file_num.parse().unwrap_or(0), - // need_override.eq("true"), - // remember.eq("true"), - // is_upload.eq("true"), - // ); - // } - // } - // } - // ** TODO ** continue - // "remove_file" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let ( - // Some(id), - // Some(path), - // Some(file_num), - // Some(is_remote), - // Some(session), - // ) = ( - // m.get("id"), - // m.get("path"), - // m.get("file_num"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.remove_file( - // id.parse().unwrap_or(0), - // path.to_owned(), - // file_num.parse().unwrap_or(0), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "read_dir_recursive" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - // m.get("id"), - // m.get("path"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.remove_dir_all( - // id.parse().unwrap_or(0), - // path.to_owned(), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "remove_all_empty_dirs" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - // m.get("id"), - // m.get("path"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.remove_dir( - // id.parse().unwrap_or(0), - // path.to_owned(), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "cancel_job" => { - // if let (Ok(id), Some(session)) = - // (value.parse(), Session::get().write().unwrap().as_mut()) - // { - // session.cancel_job(id); - // } - // } - // "create_dir" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - // m.get("id"), - // m.get("path"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.create_dir( - // id.parse().unwrap_or(0), - // path.to_owned(), - // is_remote.eq("true"), - // ); - // } - // } - // } - // Server Side - // "update_password" => { - // if value.is_empty() { - // Config::set_password(&Config::get_auto_password()); - // } else { - // Config::set_password(value); - // } - // } #[cfg(target_os = "android")] "chat_server_mode" => { if let Ok(m) = serde_json::from_str::>(value) { @@ -1105,7 +888,11 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } fn handle_query_onlines(onlines: Vec, offlines: Vec) { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "callback_query_onlines".to_owned()), ("onlines", onlines.join(",")), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 2aa4f36ec..cdfd0edce 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -138,10 +138,11 @@ pub fn get_license() -> String { } pub fn get_option(key: String) -> String { - #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::get_option(&key); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return get_option_(&key); + get_option_(&key) + // #[cfg(any(target_os = "android", target_os = "ios"))] + // return Config::get_option(&key); + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + // return get_option_(&key); } fn get_option_(key: &str) -> String { @@ -250,33 +251,31 @@ pub fn get_sound_inputs() -> Vec { } pub fn set_options(m: HashMap) { + *OPTIONS.lock().unwrap() = m.clone(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - *OPTIONS.lock().unwrap() = m.clone(); - ipc::set_options(m).ok(); - } + ipc::set_options(m).ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_options(m); } pub fn set_option(key: String, value: String) { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } + } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_options(options.clone()).ok(); #[cfg(any(target_os = "android", target_os = "ios"))] Config::set_option(key, value); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let mut options = OPTIONS.lock().unwrap(); - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; - } - } - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); - } } pub fn install_path() -> String {