diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 15d058b87..46ba90d66 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1510,3 +1510,53 @@ Pointer getOSVERSIONINFOEXPointer() { bool get kUseCompatibleUiMode => Platform.isWindows && const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); + +class ServerConfig { + late String idServer; + late String relayServer; + late String apiServer; + late String key; + + ServerConfig( + {String? idServer, String? relayServer, String? apiServer, String? key}) { + this.idServer = idServer?.trim() ?? ''; + this.relayServer = relayServer?.trim() ?? ''; + this.apiServer = apiServer?.trim() ?? ''; + this.key = key?.trim() ?? ''; + } + + /// decode from shared string (from user shared or rustdesk-server generated) + /// also see [encode] + /// throw when decoding failure + ServerConfig.decode(String msg) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + final json = jsonDecode(utf8.decode(bytes)); + + idServer = json['host'] ?? ''; + relayServer = json['relay'] ?? ''; + apiServer = json['api'] ?? ''; + key = json['key'] ?? ''; + } + + /// encode to shared string + /// also see [ServerConfig.decode] + String encode() { + Map config = {}; + config['host'] = idServer.trim(); + config['relay'] = relayServer.trim(); + config['api'] = apiServer.trim(); + config['key'] = key.trim(); + return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + .split('') + .reversed + .join(); + } + + /// from local options + ServerConfig.fromOptions(Map options) + : idServer = options['custom-rendezvous-server'] ?? "", + relayServer = options['relay-server'] ?? "", + apiServer = options['api-server'] ?? "", + key = options['key'] ?? ""; +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 613f19810..a45de24b0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -958,23 +958,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { import() { Clipboard.getData(Clipboard.kTextPlain).then((value) { - TextEditingController mytext = TextEditingController(); - String? aNullableString = ''; - aNullableString = value?.text; - mytext.text = aNullableString.toString(); - if (mytext.text.isNotEmpty) { + final text = value?.text; + if (text != null && text.isNotEmpty) { try { - Map config = jsonDecode(mytext.text); - if (config.containsKey('IdServer')) { - String id = config['IdServer'] ?? ''; - String relay = config['RelayServer'] ?? ''; - String api = config['ApiServer'] ?? ''; - String key = config['Key'] ?? ''; - idController.text = id; - relayController.text = relay; - apiController.text = api; - keyController.text = key; - Future success = set(id, relay, api, key); + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + idController.text = sc.idServer; + relayController.text = sc.relayServer; + apiController.text = sc.apiServer; + keyController.text = sc.key; + Future success = + set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); success.then((value) { if (value) { showToast( @@ -996,12 +990,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } export() { - Map config = {}; - config['IdServer'] = idController.text.trim(); - config['RelayServer'] = relayController.text.trim(); - config['ApiServer'] = apiController.text.trim(); - config['Key'] = keyController.text.trim(); - Clipboard.setData(ClipboardData(text: jsonEncode(config))); + final text = ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text) + .encode(); + debugPrint("ServerConfig export: $text"); + + Clipboard.setData(ClipboardData(text: text)); showToast(translate('Export server configuration successfully')); } @@ -1106,8 +1103,10 @@ class _AboutState extends State<_About> { const SizedBox( height: 8.0, ), - Text(translate('Version') + ': $version').marginSymmetric(vertical: 4.0), - Text(translate('Build Date') + ': $buildDate').marginSymmetric(vertical: 4.0), + Text(translate('Version') + ': $version') + .marginSymmetric(vertical: 4.0), + Text(translate('Build Date') + ': $buildDate') + .marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 810bcbca3..8778d78f7 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:zxing2/qrcode.dart'; import '../../common.dart'; -import '../../models/platform_model.dart'; +import '../widgets/dialog.dart'; class ScanPage extends StatefulWidget { @override - _ScanPageState createState() => _ScanPageState(); + State createState() => _ScanPageState(); } class _ScanPageState extends State { @@ -42,9 +41,9 @@ class _ScanPageState extends State { icon: Icon(Icons.image_search), iconSize: 32.0, onPressed: () async { - final ImagePicker _picker = ImagePicker(); + final ImagePicker picker = ImagePicker(); final XFile? file = - await _picker.pickImage(source: ImageSource.gallery); + await picker.pickImage(source: ImageSource.gallery); if (file != null) { var image = img.decodeNamedImage( File(file.path).readAsBytesSync(), file.path)!; @@ -139,158 +138,12 @@ class _ScanPageState extends State { return; } try { - Map values = json.decode(data.substring(7)); - var host = values['host'] != null ? values['host'] as String : ''; - var key = values['key'] != null ? values['key'] as String : ''; - var api = values['api'] != null ? values['api'] as String : ''; + final sc = ServerConfig.decode(data.substring(7)); Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); + showServerSettingsWithValue(sc, gFFI.dialogManager); }); } catch (e) { showToast('Invalid QR code'); } } } - -void showServerSettingsWithValue(String id, String relay, String key, - String api, OverlayDialogManager dialogManager) 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( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: idController, - decoration: InputDecoration( - labelText: translate('ID Server'), - errorText: idServerMsg), - ) - ] + - (isAndroid - ? [ - TextFormField( - controller: relayController, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg), - ) - ] - : []) + - [ - TextFormField( - controller: apiController, - decoration: InputDecoration( - labelText: translate('API Server'), - ), - 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( - initialValue: key, - decoration: InputDecoration( - labelText: 'Key', - ), - onChanged: (String? value) { - if (value != null) key = value.trim(); - }, - ), - Offstage( - offstage: !isInProgress, - child: LinearProgressIndicator()) - ])), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () async { - setState(() { - idServerMsg = null; - relayServerMsg = null; - apiServerMsg = null; - isInProgress = true; - }); - if (await validate()) { - if (id != id0) { - if (id0.isNotEmpty) { - await gFFI.userModel.logOut(); - } - bind.mainSetOption(key: "custom-rendezvous-server", value: id); - } - if (relay != relay0) { - bind.mainSetOption(key: "relay-server", value: relay); - } - if (key != key0) bind.mainSetOption(key: "key", value: key); - if (api != api0) { - bind.mainSetOption(key: "api-server", value: api); - } - close(); - } - setState(() { - isInProgress = false; - }); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - -Future validateAsync(String value) async { - value = value.trim(); - if (value.isEmpty) { - return null; - } - 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 8c7cdb5c7..9637ecb40 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -391,11 +391,7 @@ class _SettingsState extends State with WidgetsBindingObserver { void showServerSettings(OverlayDialogManager dialogManager) 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, dialogManager); + showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); } void showLanguageSettings(OverlayDialogManager dialogManager) async { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 96f96658a..d70902513 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import '../../common.dart'; @@ -236,6 +237,145 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { ])); } +void showServerSettingsWithValue( + ServerConfig serverConfig, OverlayDialogManager dialogManager) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + final oldCfg = ServerConfig.fromOptions(oldOptions); + + var isInProgress = false; + final idCtrl = TextEditingController(text: serverConfig.idServer); + final relayCtrl = TextEditingController(text: serverConfig.relayServer); + final apiCtrl = TextEditingController(text: serverConfig.apiServer); + final keyCtrl = TextEditingController(text: serverConfig.key); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + + dialogManager.show((setState, close) { + Future validate() async { + if (idCtrl.text != oldCfg.idServer) { + final res = await validateAsync(idCtrl.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + } + if (relayCtrl.text != oldCfg.relayServer) { + relayServerMsg = await validateAsync(relayCtrl.text); + if (relayServerMsg != null) return false; + } + if (apiCtrl.text != oldCfg.apiServer) { + apiServerMsg = await validateAsync(apiCtrl.text); + if (apiServerMsg != null) return false; + } + return true; + } + + return CustomAlertDialog( + title: Text(translate('ID/Relay Server')), + content: Form( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: idCtrl, + decoration: InputDecoration( + labelText: translate('ID Server'), + errorText: idServerMsg), + ) + ] + + (isAndroid + ? [ + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg), + ) + ] + : []) + + [ + TextFormField( + controller: apiCtrl, + decoration: InputDecoration( + labelText: translate('API Server'), + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.isNotEmpty) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; + }, + ), + TextFormField( + controller: keyCtrl, + decoration: InputDecoration( + labelText: 'Key', + ), + ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) + ])), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (idCtrl.text != oldCfg.idServer) { + if (oldCfg.idServer.isNotEmpty) { + await gFFI.userModel.logOut(); + } + bind.mainSetOption( + key: "custom-rendezvous-server", value: idCtrl.text); + } + if (relayCtrl.text != oldCfg.relayServer) { + bind.mainSetOption(key: "relay-server", value: relayCtrl.text); + } + if (keyCtrl.text != oldCfg.key) { + bind.mainSetOption(key: "key", value: keyCtrl.text); + } + if (apiCtrl.text != oldCfg.apiServer) { + bind.mainSetOption(key: "api-server", value: apiCtrl.text); + } + close(); + } + setState(() { + isInProgress = false; + }); + }, + child: Text(translate('OK')), + ), + ], + ); + }); +} + +Future validateAsync(String value) async { + value = value.trim(); + if (value.isEmpty) { + return null; + } + final res = await bind.mainTestIfValidServer(server: value); + return res.isEmpty ? null : res; +} + class PasswordWidget extends StatefulWidget { PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) : super(key: key); @@ -285,7 +425,7 @@ class _PasswordWidgetState extends State { color: Theme.of(context).primaryColorDark, ), onPressed: () { - // Update the state i.e. toogle the state of passwordVisible variable + // Update the state i.e. toggle the state of passwordVisible variable setState(() { _passwordVisible = !_passwordVisible; });