diff --git a/README.md b/README.md index fbbc1b60b..6598dd51c 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTI Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Singapore | Vultr | 1 vCPU / 1GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | ## Dependencies diff --git a/flutter/.gitignore b/flutter/.gitignore index fdd17a5ed..af69247b0 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -45,7 +45,7 @@ jniLibs # flutter rust bridge # Flutter Generated Files -**/flutter/GeneratedPluginRegistrant.swift +**/GeneratedPluginRegistrant.swift **/flutter/generated_plugin_registrant.cc **/flutter/generated_plugin_registrant.h **/flutter/generated_plugins.cmake diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 17e45ba95..75328c840 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget { void msgBox( String type, String title, String text, OverlayDialogManager dialogManager, {bool? hasCancel}) { - var wrap = (String text, void Function() onPressed) => ButtonTheme( + dialogManager.dismissAll(); + List buttons = []; + if (type != "connecting" && type != "success" && !type.contains("nook")) { + buttons.insert( + 0, + msgBoxButton(translate('OK'), () { + dialogManager.dismissAll(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom")) { + closeConnection(); + } + })); + } + hasCancel ??= !type.contains("error") && + !type.contains("nocancel") && + type != "restarting"; + if (hasCancel) { + buttons.insert( + 0, + msgBoxButton(translate('Cancel'), () { + dialogManager.dismissAll(); + })); + } + // TODO: test this button + if (type.contains("hasclose")) { + buttons.insert( + 0, + msgBoxButton(translate('Close'), () { + dialogManager.dismissAll(); + })); + } + dialogManager.show((setState, close) => CustomAlertDialog( + title: _msgBoxTitle(title), + content: Text(translate(text), style: TextStyle(fontSize: 15)), + actions: buttons)); +} + +Widget msgBoxButton(String text, void Function() onPressed) { + return ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, //limits the touch area to the button area @@ -439,41 +477,16 @@ void msgBox( onPressed: onPressed, child: Text(translate(text), style: TextStyle(color: MyTheme.accent)))); +} +Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21)); + +void msgBoxCommon(OverlayDialogManager dialogManager, String title, + Widget content, List buttons) { dialogManager.dismissAll(); - List buttons = []; - if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { - buttons.insert( - 0, - wrap(translate('OK'), () { - dialogManager.dismissAll(); - closeConnection(); - })); - } - if (hasCancel == null) { - // hasCancel = type != 'error'; - hasCancel = type.indexOf("error") < 0 && - type.indexOf("nocancel") < 0 && - type != "restarting"; - } - if (hasCancel) { - buttons.insert( - 0, - wrap(translate('Cancel'), () { - dialogManager.dismissAll(); - })); - } - // TODO: test this button - if (type.indexOf("hasclose") >= 0) { - buttons.insert( - 0, - wrap(translate('Close'), () { - dialogManager.dismissAll(); - })); - } dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate(title), style: TextStyle(fontSize: 21)), - content: Text(translate(text), style: TextStyle(fontSize: 15)), + title: _msgBoxTitle(title), + content: content, actions: buttons)); } @@ -492,13 +505,13 @@ const G = M * K; String readableFileSize(double size) { if (size < K) { - return size.toStringAsFixed(2) + " B"; + return "${size.toStringAsFixed(2)} B"; } else if (size < M) { - return (size / K).toStringAsFixed(2) + " KB"; + return "${(size / K).toStringAsFixed(2)} KB"; } else if (size < G) { - return (size / M).toStringAsFixed(2) + " MB"; + return "${(size / M).toStringAsFixed(2)} MB"; } else { - return (size / G).toStringAsFixed(2) + " GB"; + return "${(size / G).toStringAsFixed(2)} GB"; } } @@ -661,8 +674,6 @@ Future initGlobalFFI() async { debugPrint("_globalFFI init end"); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); - // trigger connection status updater - await bind.mainCheckConnectStatus(); // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); } diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart new file mode 100644 index 000000000..7232cb6ad --- /dev/null +++ b/flutter/lib/common/shared_state.dart @@ -0,0 +1,87 @@ +import 'package:get/get.dart'; + +import '../consts.dart'; + +class PrivacyModeState { + static String tag(String id) => 'privacy_mode_$id'; + + static void init(String id) { + final RxBool state = false.obs; + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class BlockInputState { + static String tag(String id) => 'block_input_$id'; + + static void init(String id) { + final RxBool state = false.obs; + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class CurrentDisplayState { + static String tag(String id) => 'current_display_$id'; + + static void init(String id) { + final RxInt state = RxInt(0); + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxInt find(String id) => Get.find(tag: tag(id)); +} + +class ConnectionType { + final Rx _secure = kInvalidValueStr.obs; + final Rx _direct = kInvalidValueStr.obs; + + Rx get secure => _secure; + Rx get direct => _direct; + + static String get strSecure => 'secure'; + static String get strInsecure => 'insecure'; + static String get strDirect => ''; + static String get strIndirect => '_relay'; + + void setSecure(bool v) { + _secure.value = v ? strSecure : strInsecure; + } + + void setDirect(bool v) { + _direct.value = v ? strDirect : strIndirect; + } + + bool isValid() { + return _secure.value != kInvalidValueStr && + _direct.value != kInvalidValueStr; + } +} + +class ConnectionTypeState { + static String tag(String id) => 'connection_type_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final ConnectionType collectionType = ConnectionType(); + Get.put(collectionType, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static ConnectionType find(String id) => + Get.find(tag: tag(id)); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 000a1cb54..95a6faaa2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,8 +4,14 @@ const double kDesktopRemoteTabBarHeight = 28.0; const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeDesktopPortForward = "port forward"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; -const int kDefaultDisplayWidth = 1280; -const int kDefaultDisplayHeight = 720; +const int kMobileDefaultDisplayWidth = 720; +const int kMobileDefaultDisplayHeight = 1280; + +const int kDesktopDefaultDisplayWidth = 1080; +const int kDesktopDefaultDisplayHeight = 720; + +const kInvalidValueStr = "InvalidValueStr"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 29219df2a..5fd6b4a28 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -33,7 +33,7 @@ class _ConnectionPageState extends State { final _idController = TextEditingController(); /// Update url. If it's not null, means an update is available. - var _updateUrl = ''; + final _updateUrl = ''; Timer? _updateTimer; @@ -92,7 +92,7 @@ class _ConnectionPageState extends State { if (snapshot.hasData) { return snapshot.data!; } else { - return Offstage(); + return const Offstage(); } }), ], @@ -110,7 +110,7 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - var id = _idController.text.trim(); + final id = _idController.text.trim(); connect(id, isFileTransfer: isFileTransfer); } @@ -120,9 +120,9 @@ class _ConnectionPageState extends State { if (id == '') return; id = id.replaceAll(' ', ''); if (isFileTransfer) { - await rustDeskWinManager.new_file_transfer(id); + await rustDeskWinManager.newFileTransfer(id); } else { - await rustDeskWinManager.new_remote_desktop(id); + await rustDeskWinManager.newRemoteDesktop(id); } FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { @@ -233,7 +233,6 @@ class _ConnectionPageState extends State { }, child: Container( height: 24, - width: 72, alignment: Alignment.center, decoration: BoxDecoration( color: ftPressed.value @@ -257,7 +256,7 @@ class _ConnectionPageState extends State { color: ftPressed.value ? MyTheme.color(context).bg : MyTheme.color(context).text), - ), + ).marginSymmetric(horizontal: 12), ), )), SizedBox( @@ -272,7 +271,6 @@ class _ConnectionPageState extends State { onTap: onConnect, child: Container( height: 24, - width: 65, decoration: BoxDecoration( color: connPressed.value ? MyTheme.accent @@ -289,12 +287,12 @@ class _ConnectionPageState extends State { child: Center( child: Text( translate( - "Connection", + "Connect", ), style: TextStyle( fontSize: 12, color: MyTheme.color(context).bg), ), - ), + ).marginSymmetric(horizontal: 12), ), ), ), diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index be7c76f2a..5687c5c7e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -3,14 +3,13 @@ import 'dart:convert'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import '../../models/model.dart'; - class ConnectionTabPage extends StatefulWidget { final Map params; @@ -22,26 +21,27 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController()); - static final Rx _fullscreenID = "".obs; - static final IconData selectedIcon = Icons.desktop_windows_sharp; - static final IconData unselectedIcon = Icons.desktop_windows_outlined; + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { - if (params['id'] != null) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); + final peerId = params['id']; + if (peerId != null) { + ConnectionTypeState.init(peerId); tabController.add(TabInfo( - key: params['id'], - label: params['id'], + key: peerId, + label: peerId, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: RemotePage( - key: ValueKey(params['id']), - id: params['id'], - tabBarHeight: - _fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - ))); + page: Obx(() => RemotePage( + key: ValueKey(peerId), + id: peerId, + tabBarHeight: + fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + )))); } } @@ -54,33 +54,27 @@ class _ConnectionTabPageState extends State { rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + + final RxBool fullscreen = Get.find(tag: 'fullscreen'); // for simplify, just replace connectionId if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); + ConnectionTypeState.init(id); tabController.add(TabInfo( key: id, label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: RemotePage( - key: ValueKey(id), - id: id, - tabBarHeight: _fullscreenID.value.isNotEmpty - ? 0 - : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - ))); + page: Obx(() => RemotePage( + key: ValueKey(id), + id: id, + tabBarHeight: + fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + )))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.forEach((tab) { - print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.clear(); } }); } @@ -88,36 +82,79 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); - return SubWindowDragToResizeArea( - windowId: windowId(), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Obx(() => DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - showTabBar: _fullscreenID.value.isEmpty, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), - pageViewBuilder: (pageView) { - WindowController.fromWindowId(windowId()) - .setFullscreen(_fullscreenID.value.isNotEmpty); - return pageView; - }, - ))), - ), - ); + final RxBool fullscreen = Get.find(tag: 'fullscreen'); + return Obx(() => SubWindowDragToResizeArea( + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + windowId: windowId(), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Obx(() => DesktopTab( + controller: tabController, + theme: theme, + tabType: DesktopTabType.remoteScreen, + showTabBar: fullscreen.isFalse, + onClose: () { + tabController.clear(); + }, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(fullscreen.isTrue); + return pageView; + }, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final msgDirect = translate( + connectionType.direct.value == + ConnectionType.strDirect + ? 'Direct Connection' + : 'Relay Connection'); + final msgSecure = translate( + connectionType.secure.value == + ConnectionType.strSecure + ? 'Secure Connection' + : 'Insecure Connection'); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgDirect\n$msgSecure', + child: Image.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.png', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + ], + ); + } + }), + ))), + ), + )); } void onRemoveId(String id) { - ffi(id).close(); - if (tabController.state.value.tabs.length == 0) { - WindowController.fromWindowId(windowId()).close(); + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).hide(); } + ConnectionTypeState.delete(id); } int windowId() { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 12f17c95e..632177e29 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -806,6 +806,8 @@ Future loginDialog() async { var userNameMsg = ""; String pass = ""; var passMsg = ""; + var userContontroller = TextEditingController(text: userName); + var pwdController = TextEditingController(text: pass); var isInProgress = false; var completer = Completer(); @@ -833,13 +835,10 @@ Future loginDialog() async { ), Expanded( child: TextField( - onChanged: (s) { - userName = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), - controller: TextEditingController(text: userName), + controller: userContontroller, ), ), ], @@ -859,13 +858,10 @@ Future loginDialog() async { Expanded( child: TextField( obscureText: true, - onChanged: (s) { - pass = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: passMsg.isNotEmpty ? passMsg : null), - controller: TextEditingController(text: pass), + controller: pwdController, ), ), ], @@ -896,8 +892,8 @@ Future loginDialog() async { isInProgress = false; }); }; - userName = userName; - pass = pass; + userName = userContontroller.text; + pass = pwdController.text; if (userName.isEmpty) { userNameMsg = translate("Username missed"); cancel(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4f86974f1..120f8bc7a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1025,7 +1025,6 @@ class _ComboBox extends StatelessWidget { void changeServer() async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); - print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; String relayServer = oldOptions['relay-server'] ?? ""; @@ -1033,6 +1032,10 @@ void changeServer() async { String apiServer = oldOptions['api-server'] ?? ""; var apiServerMsg = ""; var key = oldOptions['key'] ?? ""; + var idController = TextEditingController(text: idServer); + var relayController = TextEditingController(text: relayServer); + var apiController = TextEditingController(text: apiServer); + var keyController = TextEditingController(text: key); var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1057,13 +1060,10 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - idServer = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: idServerMsg.isNotEmpty ? idServerMsg : null), - controller: TextEditingController(text: idServer), + controller: idController, ), ), ], @@ -1082,14 +1082,11 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - relayServer = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: relayServerMsg.isNotEmpty ? relayServerMsg : null), - controller: TextEditingController(text: relayServer), + controller: relayController, ), ), ], @@ -1108,14 +1105,11 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - apiServer = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: apiServerMsg.isNotEmpty ? apiServerMsg : null), - controller: TextEditingController(text: apiServer), + controller: apiController, ), ), ], @@ -1134,13 +1128,10 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - key = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), ), - controller: TextEditingController(text: key), + controller: keyController, ), ), ], @@ -1171,10 +1162,10 @@ void changeServer() async { isInProgress = false; }); }; - idServer = idServer.trim(); - relayServer = relayServer.trim(); - apiServer = apiServer.trim(); - key = key.trim(); + idServer = idController.text.trim(); + relayServer = relayController.text.trim(); + apiServer = apiController.text.trim().toLowerCase(); + key = keyController.text.trim(); if (idServer.isNotEmpty) { idServerMsg = translate( @@ -1230,6 +1221,7 @@ void changeWhiteList() async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); + var controller = TextEditingController(text: newWhiteListField); var msg = ""; var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1246,15 +1238,12 @@ void changeWhiteList() async { children: [ Expanded( child: TextField( - onChanged: (s) { - newWhiteListField = s; - }, maxLines: null, decoration: InputDecoration( border: OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), - controller: TextEditingController(text: newWhiteListField), + controller: controller, ), ), ], @@ -1277,7 +1266,7 @@ void changeWhiteList() async { msg = ""; isInProgress = true; }); - newWhiteListField = newWhiteListField.trim(); + newWhiteListField = controller.text.trim(); var newWhiteList = ""; if (newWhiteListField.isEmpty) { // pass @@ -1319,6 +1308,9 @@ void changeSocks5Proxy() async { username = socks[1]; password = socks[2]; } + var proxyController = TextEditingController(text: proxy); + var userController = TextEditingController(text: username); + var pwdController = TextEditingController(text: password); var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1343,13 +1335,10 @@ void changeSocks5Proxy() async { ), Expanded( child: TextField( - onChanged: (s) { - proxy = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), - controller: TextEditingController(text: proxy), + controller: proxyController, ), ), ], @@ -1368,13 +1357,10 @@ void changeSocks5Proxy() async { ), Expanded( child: TextField( - onChanged: (s) { - username = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), ), - controller: TextEditingController(text: username), + controller: userController, ), ), ], @@ -1393,13 +1379,10 @@ void changeSocks5Proxy() async { ), Expanded( child: TextField( - onChanged: (s) { - password = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), ), - controller: TextEditingController(text: password), + controller: pwdController, ), ), ], @@ -1428,9 +1411,9 @@ void changeSocks5Proxy() async { isInProgress = false; }); }; - proxy = proxy.trim(); - username = username.trim(); - password = password.trim(); + proxy = proxyController.text.trim(); + username = userController.text.trim(); + password = pwdController.text.trim(); if (proxy.isNotEmpty) { proxyMsg = diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 4a2fdb7d2..57ee43e14 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; class DesktopTabPage extends StatefulWidget { @@ -33,26 +34,29 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final dark = isDarkTheme(); - return DragToResizeArea( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - isMainWindow: true, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - onTap: onAddSetting, - is_close: false, - ), - )), - ), - ); + RxBool fullscreen = false.obs; + Get.put(fullscreen, tag: 'fullscreen'); + return Obx(() => DragToResizeArea( + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + tabType: DesktopTabType.main, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + onTap: onAddSetting, + is_close: false, + ), + )), + ), + )); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 09577128f..6c8b58a30 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -20,12 +19,13 @@ class FileManagerTabPage extends StatefulWidget { } class _FileManagerTabPageState extends State { - final tabController = Get.put(DesktopTabController()); + DesktopTabController get tabController => Get.find(); static final IconData selectedIcon = Icons.file_copy_sharp; static final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { + Get.put(DesktopTabController()); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -42,7 +42,7 @@ class _FileManagerTabPageState extends State { rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( - "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + "call ${call.method} with args ${call.arguments} from window ${fromWindowId} to ${windowId()}"); // for simplify, just replace connectionId if (call.method == "new_file_transfer") { final args = jsonDecode(call.arguments); @@ -55,21 +55,15 @@ class _FileManagerTabPageState extends State { unselectedIcon: unselectedIcon, page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.forEach((tab) { - print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.clear(); } }); } @override Widget build(BuildContext context) { - final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); + final theme = + isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( @@ -80,7 +74,10 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.fileTransfer, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), @@ -90,9 +87,8 @@ class _FileManagerTabPageState extends State { } void onRemoveId(String id) { - ffi("ft_$id").close(); - if (tabController.state.value.tabs.length == 0) { - WindowController.fromWindowId(windowId()).close(); + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).hide(); } } diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart new file mode 100644 index 000000000..6cfd0cdb2 --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -0,0 +1,348 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:wakelock/wakelock.dart'; + +const double _kColumn1Width = 30; +const double _kColumn4Width = 100; +const double _kRowHeight = 50; +const double _kTextLeftMargin = 20; + +class _PortForward { + int localPort; + String remoteHost; + int remotePort; + + _PortForward.fromJson(List json) + : localPort = json[0] as int, + remoteHost = json[1] as String, + remotePort = json[2] as int; +} + +class PortForwardPage extends StatefulWidget { + const PortForwardPage({Key? key, required this.id, required this.isRDP}) + : super(key: key); + final String id; + final bool isRDP; + + @override + State createState() => _PortForwardPageState(); +} + +class _PortForwardPageState extends State + with AutomaticKeepAliveClientMixin { + final bool isRdp = false; + final TextEditingController localPortController = TextEditingController(); + final TextEditingController remoteHostController = TextEditingController(); + final TextEditingController remotePortController = TextEditingController(); + RxList<_PortForward> pfs = RxList.empty(growable: true); + late FFI _ffi; + + @override + void initState() { + super.initState(); + _ffi = FFI(); + _ffi.connect(widget.id, isPortForward: true); + Get.put(_ffi, tag: 'pf_${widget.id}'); + if (!Platform.isLinux) { + Wakelock.enable(); + } + print("init success with id ${widget.id}"); + } + + @override + void dispose() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'pf_${widget.id}'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: MyTheme.color(context).grayBg, + body: FutureBuilder(future: () async { + if (!isRdp) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, color: MyTheme.color(context).grayBg!)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + border: Border.all(width: 1, color: MyTheme.border)), + child: + widget.isRDP ? buildRdp(context) : buildTunnel(context), + ), + ), + ], + ), + ); + } + return const Offstage(); + }), + ); + } + + buildPrompt(BuildContext context) { + return Obx(() => Offstage( + offstage: pfs.isEmpty && !widget.isRDP, + child: Container( + height: 45, + color: const Color(0xFF007F00), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate('Listening ...'), + style: const TextStyle(fontSize: 16, color: Colors.white), + ), + Text( + translate('not_close_tcp_tip'), + style: const TextStyle( + fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2), + ) + ])).marginOnly(bottom: 8), + )); + } + + buildTunnel(BuildContext context) { + text(String lable) => Expanded( + child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); + + return Theme( + data: Theme.of(context) + .copyWith(backgroundColor: MyTheme.color(context).bg), + child: Obx(() => ListView.builder( + itemCount: pfs.length + 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: MyTheme.color(context).grayBg, + child: Row(children: [ + text('Local Port'), + const SizedBox(width: _kColumn1Width), + text('Remote Host'), + text('Remote Port'), + SizedBox( + width: _kColumn4Width, child: Text(translate('Action'))) + ]), + ); + } else if (index == 1) { + return buildTunnelAddRow(context); + } else { + return buildTunnelDataRow(context, pfs[index - 2], index - 2); + } + }))), + ); + } + + buildTunnelAddRow(BuildContext context) { + var portInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')) + ]; + + return Container( + height: _kRowHeight, + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Row(children: [ + buildTunnelInputCell(context, + controller: localPortController, + inputFormatters: portInputFormatter), + const SizedBox( + width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)), + buildTunnelInputCell(context, + controller: remoteHostController, hint: 'localhost'), + buildTunnelInputCell(context, + controller: remotePortController, + inputFormatters: portInputFormatter), + SizedBox( + width: _kColumn4Width, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, side: const BorderSide(color: MyTheme.border)), + onPressed: () async { + int? localPort = int.tryParse(localPortController.text); + int? remotePort = int.tryParse(remotePortController.text); + if (localPort != null && + remotePort != null && + (remoteHostController.text.isEmpty || + remoteHostController.text.trim().isNotEmpty)) { + await bind.sessionAddPortForward( + id: 'pf_${widget.id}', + localPort: localPort, + remoteHost: remoteHostController.text.trim().isEmpty + ? 'localhost' + : remoteHostController.text.trim(), + remotePort: remotePort); + localPortController.clear(); + remoteHostController.clear(); + remotePortController.clear(); + refreshTunnelConfig(); + } + }, + child: Text( + translate('Add'), + ), + ).marginAll(10), + ), + ]), + ); + } + + buildTunnelInputCell(BuildContext context, + {required TextEditingController controller, + List? inputFormatters, + String? hint}) { + return Expanded( + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + cursorColor: MyTheme.color(context).text, + cursorHeight: 20, + cursorWidth: 1, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: MyTheme.color(context).border!)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: MyTheme.color(context).border!)), + fillColor: MyTheme.color(context).bg, + contentPadding: const EdgeInsets.all(10), + hintText: hint, + hintStyle: TextStyle( + color: MyTheme.color(context).placeholder, fontSize: 16)), + style: TextStyle(color: MyTheme.color(context).text, fontSize: 16), + ).marginAll(10), + ); + } + + Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { + text(String lable) => Expanded( + child: Text(lable, style: const TextStyle(fontSize: 20)) + .marginOnly(left: _kTextLeftMargin)); + + return Container( + height: _kRowHeight, + decoration: BoxDecoration( + color: index % 2 == 0 + ? isDarkTheme() + ? const Color(0xFF202020) + : const Color(0xFFF4F5F6) + : MyTheme.color(context).bg), + child: Row(children: [ + text(pf.localPort.toString()), + const SizedBox(width: _kColumn1Width), + text(pf.remoteHost), + text(pf.remotePort.toString()), + SizedBox( + width: _kColumn4Width, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await bind.sessionRemovePortForward( + id: 'pf_${widget.id}', localPort: pf.localPort); + refreshTunnelConfig(); + }, + ), + ), + ]), + ); + } + + void refreshTunnelConfig() async { + String peer = await bind.mainGetPeer(id: widget.id); + Map config = jsonDecode(peer); + List infos = config['port_forwards'] as List; + List<_PortForward> result = List.empty(growable: true); + for (var e in infos) { + result.add(_PortForward.fromJson(e)); + } + pfs.value = result; + } + + buildRdp(BuildContext context) { + text1(String lable) => + Expanded(child: Text(lable).marginOnly(left: _kTextLeftMargin)); + text2(String lable) => Expanded( + child: Text( + lable, + style: TextStyle(fontSize: 20), + ).marginOnly(left: _kTextLeftMargin)); + return Theme( + data: Theme.of(context) + .copyWith(backgroundColor: MyTheme.color(context).bg), + child: ListView.builder( + itemCount: 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: MyTheme.color(context).grayBg, + child: Row(children: [ + text1('Local Port'), + const SizedBox(width: _kColumn1Width), + text1('Remote Host'), + text1('Remote Port'), + ]), + ); + } else { + return Container( + height: _kRowHeight, + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Row(children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 120, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + side: const BorderSide(color: MyTheme.border)), + onPressed: () {}, + child: Text( + translate('New RDP'), + style: TextStyle( + fontWeight: FontWeight.w300, fontSize: 14), + ), + ).marginSymmetric(vertical: 10), + ).marginOnly(left: 20), + ), + ), + const SizedBox( + width: _kColumn1Width, + child: Icon(Icons.arrow_forward_sharp)), + text2('localhost'), + text2('RDP'), + ]), + ); + } + })), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart new file mode 100644 index 000000000..1e2c8e2bc --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +class PortForwardTabPage extends StatefulWidget { + final Map params; + + const PortForwardTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _PortForwardTabPageState(params); +} + +class _PortForwardTabPageState extends State { + final tabController = Get.put(DesktopTabController()); + late final bool isRDP; + + static const IconData selectedIcon = Icons.forward_sharp; + static const IconData unselectedIcon = Icons.forward_outlined; + + _PortForwardTabPageState(Map params) { + isRDP = params['isRDP']; + tabController.add(TabInfo( + key: params['id'], + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage( + key: ValueKey(params['id']), + id: params['id'], + isRDP: isRDP, + ))); + } + + @override + void initState() { + super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + debugPrint( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + // for simplify, just replace connectionId + if (call.method == "new_port_forward") { + final args = jsonDecode(call.arguments); + final id = args['id']; + final isRDP = args['isRDP']; + window_on_top(windowId()); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage(id: id, isRDP: isRDP))); + } else if (call.method == "onDestroy") { + tabController.clear(); + } + }); + } + + @override + Widget build(BuildContext context) { + final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); + return SubWindowDragToResizeArea( + windowId: windowId(), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: theme, + tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward, + onClose: () { + tabController.clear(); + }, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + )), + ), + ); + } + + void onRemoveId(String id) { + ffi("pf_$id").close(); + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).hide(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c121fe1ab..c3f4b3773 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -5,32 +5,32 @@ import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; // import 'package:window_manager/window_manager.dart'; +import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../models/chat_model.dart'; +import '../../common/shared_state.dart'; final initText = '\1' * 1024; class RemotePage extends StatefulWidget { - RemotePage( - {Key? key, - required this.id, - required this.tabBarHeight, - required this.fullscreenID}) - : super(key: key); + RemotePage({ + Key? key, + required this.id, + required this.tabBarHeight, + }) : super(key: key); final String id; final double tabBarHeight; - final Rx fullscreenID; @override _RemotePageState createState() => _RemotePageState(); @@ -41,7 +41,7 @@ class _RemotePageState extends State Timer? _timer; bool _showBar = !isWebDesktop; String _value = ''; - var _cursorOverImage = false.obs; + final _cursorOverImage = false.obs; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -50,11 +50,27 @@ class _RemotePageState extends State late FFI _ffi; + void _updateTabBarHeight() { + _ffi.canvasModel.tabBarHeight = widget.tabBarHeight; + } + + void _initStates(String id) { + PrivacyModeState.init(id); + BlockInputState.init(id); + CurrentDisplayState.init(id); + } + + void _removeStates(String id) { + PrivacyModeState.delete(id); + BlockInputState.delete(id); + CurrentDisplayState.delete(id); + } + @override void initState() { super.initState(); _ffi = FFI(); - _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; + _updateTabBarHeight(); Get.put(_ffi, tag: widget.id); _ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -70,11 +86,12 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); + _initStates(widget.id); } @override void dispose() { - print("REMOTE PAGE dispose ${widget.id}"); + debugPrint("REMOTE PAGE dispose ${widget.id}"); hideMobileActionsOverlay(); _ffi.listenToMouse(false); _mobileFocusNode.dispose(); @@ -90,6 +107,7 @@ class _RemotePageState extends State // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); + _removeStates(widget.id); } void resetTool() { @@ -187,19 +205,19 @@ class _RemotePageState extends State return Scaffold( backgroundColor: MyTheme.color(context).bg, // resizeToAvoidBottomInset: true, - floatingActionButton: _showBar - ? null - : FloatingActionButton( - mini: true, - child: Icon(Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - _showBar = !_showBar; - }); - }), - bottomNavigationBar: - _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, + // floatingActionButton: _showBar + // ? null + // : FloatingActionButton( + // mini: true, + // child: Icon(Icons.expand_less), + // backgroundColor: MyTheme.accent, + // onPressed: () { + // setState(() { + // _showBar = !_showBar; + // }); + // }), + // bottomNavigationBar: + // _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -217,6 +235,7 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); + _updateTabBarHeight(); return WillPopScope( onWillPop: () async { clientClose(_ffi.dialogManager); @@ -337,6 +356,7 @@ class _RemotePageState extends State } Widget? getBottomAppBar(FfiModel ffiModel) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); return MouseRegion( cursor: SystemMouseCursors.basic, child: BottomAppBar( @@ -371,15 +391,11 @@ class _RemotePageState extends State : [ IconButton( color: Colors.white, - icon: Icon(widget.fullscreenID.value.isEmpty + icon: Icon(fullscreen.isTrue ? Icons.fullscreen : Icons.close_fullscreen), onPressed: () { - if (widget.fullscreenID.value.isEmpty) { - widget.fullscreenID.value = widget.id; - } else { - widget.fullscreenID.value = ""; - } + fullscreen.value = !fullscreen.value; }, ) ]) + @@ -452,7 +468,7 @@ class _RemotePageState extends State } if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -466,7 +482,7 @@ class _RemotePageState extends State } if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousedown'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -474,7 +490,7 @@ class _RemotePageState extends State if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mouseup'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -482,7 +498,7 @@ class _RemotePageState extends State if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -548,6 +564,10 @@ class _RemotePageState extends State )); } paints.add(QualityMonitor(_ffi.qualityMonitorModel)); + paints.add(RemoteMenubar( + id: widget.id, + ffi: _ffi, + )); return Stack( children: paints, ); @@ -717,11 +737,11 @@ class ImagePaint extends StatelessWidget { width: c.getDisplayWidth() * s, height: c.getDisplayHeight() * s, child: CustomPaint( - painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s), + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); return Center( child: NotificationListener( - onNotification: (_notification) { + onNotification: (notification) { final percentX = _horizontal.position.extentBefore / (_horizontal.position.extentBefore + _horizontal.position.extentInside + @@ -744,8 +764,8 @@ class ImagePaint extends StatelessWidget { width: c.size.width, height: c.size.height, child: CustomPaint( - painter: new ImagePainter( - image: m.image, x: c.x / s, y: c.y / s, scale: s), + painter: + ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), )); return _buildListener(imageWidget); } @@ -799,7 +819,7 @@ class CursorPaint extends StatelessWidget { // final adjust = m.adjustForKeyboard(); var s = c.scale; return CustomPaint( - painter: new ImagePainter( + painter: ImagePainter( image: m.image, x: m.x * s - m.hotx + c.x, y: m.y * s - m.hoty + c.y, @@ -824,15 +844,16 @@ class ImagePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { if (image == null) return; + if (x.isNaN || y.isNaN) return; canvas.scale(scale, scale); // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html - var paint = new Paint(); + var paint = Paint(); paint.filterQuality = FilterQuality.medium; if (scale > 10.00000) { paint.filterQuality = FilterQuality.high; } - canvas.drawImage(image!, new Offset(x, y), paint); + canvas.drawImage(image!, Offset(x, y), paint); } @override diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d96efc710..e7922403b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -111,7 +111,7 @@ class ConnectionManagerState extends State { showMaximize: false, showMinimize: false, controller: serverModel.tabController, - isMainWindow: true, + tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: client.isFileTransfer, child: IconButton( - onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), + onPressed: () => checkClickTime( + client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), icon: Icon(Icons.message_outlined), ), ) @@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), padding: EdgeInsets.all(4.0), child: InkWell( - onTap: () => onTap?.call(!enabled), + onTap: () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)), child: Image( image: icon, width: 50, @@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => handleDisconnect(context), + onTap: () => + checkClickTime(client.id, () => handleDisconnect(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => handleAccept(context), + onTap: () => + checkClickTime(client.id, () => handleAccept(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget { borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey)), child: InkWell( - onTap: () => handleDisconnect(context), + onTap: () => + checkClickTime(client.id, () => handleDisconnect(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -572,3 +577,12 @@ Widget clientInfo(Client client) { ), ])); } + +void checkClickTime(int id, Function() callback) async { + var clickCallbackTime = DateTime.now().millisecondsSinceEpoch; + await bind.cmCheckClickTime(connId: id); + Timer(const Duration(milliseconds: 120), () async { + var d = clickCallbackTime - await bind.cmGetClickTime(); + if (d > 120) callback(); + }); +} diff --git a/flutter/lib/desktop/screen/desktop_port_forward_screen.dart b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart new file mode 100644 index 000000000..c7c163a57 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file port forward screen +class DesktopPortForwardScreen extends StatelessWidget { + final Map params; + + const DesktopPortForwardScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + body: PortForwardTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index 4e941ed7c..5b5dd07c2 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen @@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget { @override Widget build(BuildContext context) { + RxBool fullscreen = false.obs; + Get.put(fullscreen, tag: 'fullscreen'); return MultiProvider( providers: [ ChangeNotifierProvider.value(value: gFFI.ffiModel), diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart new file mode 100644 index 000000000..a9aec932b --- /dev/null +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -0,0 +1,1321 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/material.dart'; + +// Examples can assume: +// enum Commands { heroAndScholar, hurricaneCame } +// late bool _heroAndScholar; +// late dynamic _selection; +// late BuildContext context; +// void setState(VoidCallback fn) { } +// enum Menu { itemOne, itemTwo, itemThree, itemFour } + +const Duration _kMenuDuration = Duration(milliseconds: 300); +const double _kMenuCloseIntervalEnd = 2.0 / 3.0; +const double _kMenuHorizontalPadding = 16.0; +const double _kMenuDividerHeight = 16.0; +//const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; +const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; +const double _kMenuMaxWidth = double.infinity; +// const double _kMenuVerticalPadding = 8.0; +const double _kMenuVerticalPadding = 0.0; +const double _kMenuWidthStep = 0.0; +//const double _kMenuScreenPadding = 8.0; +const double _kMenuScreenPadding = 0.0; +const double _kDefaultIconSize = 24.0; + +/// Used to configure how the [PopupMenuButton] positions its popup menu. +enum PopupMenuPosition { + /// Menu is positioned over the anchor. + over, + + /// Menu is positioned under the anchor. + under, + + // Only support right side (TextDirection.ltr) for now + /// Menu is positioned over side the anchor + overSide, + + // Only support right side (TextDirection.ltr) for now + /// Menu is positioned under side the anchor + underSide, +} + +/// A base class for entries in a material design popup menu. +/// +/// The popup menu widget uses this interface to interact with the menu items. +/// To show a popup menu, use the [showMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// The type `T` is the type of the value(s) the entry represents. All the +/// entries in a given menu must represent values with consistent types. +/// +/// A [PopupMenuEntry] may represent multiple values, for example a row with +/// several icons, or a single entry, for example a menu item with an icon (see +/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]). +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +abstract class PopupMenuEntry extends StatefulWidget { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const PopupMenuEntry({Key? key}) : super(key: key); + + /// The amount of vertical space occupied by this entry. + /// + /// This value is used at the time the [showMenu] method is called, if the + /// `initialValue` argument is provided, to determine the position of this + /// entry when aligning the selected entry over the given `position`. It is + /// otherwise ignored. + double get height; + + /// Whether this entry represents a particular value. + /// + /// This method is used by [showMenu], when it is called, to align the entry + /// representing the `initialValue`, if any, to the given `position`, and then + /// later is called on each entry to determine if it should be highlighted (if + /// the method returns true, the entry will have its background color set to + /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then + /// this method is not called. + /// + /// If the [PopupMenuEntry] represents a single value, this should return true + /// if the argument matches that value. If it represents multiple values, it + /// should return true if the argument matches any of them. + bool represents(T? value); +} + +/// A horizontal divider in a material design popup menu. +/// +/// This widget adapts the [Divider] for use in popup menus. +/// +/// See also: +/// +/// * [PopupMenuItem], for the kinds of items that this widget divides. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class PopupMenuDivider extends PopupMenuEntry { + /// Creates a horizontal divider for a popup menu. + /// + /// By default, the divider has a height of 16 logical pixels. + const PopupMenuDivider({Key? key, this.height = _kMenuDividerHeight}) + : super(key: key); + + /// The height of the divider entry. + /// + /// Defaults to 16 pixels. + @override + final double height; + + @override + bool represents(void value) => false; + + @override + State createState() => _PopupMenuDividerState(); +} + +class _PopupMenuDividerState extends State { + @override + Widget build(BuildContext context) => Divider(height: widget.height); +} + +// This widget only exists to enable _PopupMenuRoute to save the sizes of +// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the +// y coordinate of the menu's origin so that the center of selected menu +// item lines up with the center of its PopupMenuButton. +class _MenuItem extends SingleChildRenderObjectWidget { + const _MenuItem({ + Key? key, + required this.onLayout, + required Widget? child, + }) : assert(onLayout != null), + super(key: key, child: child); + + final ValueChanged onLayout; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderMenuItem(onLayout); + } + + @override + void updateRenderObject( + BuildContext context, covariant _RenderMenuItem renderObject) { + renderObject.onLayout = onLayout; + } +} + +class _RenderMenuItem extends RenderShiftedBox { + _RenderMenuItem(this.onLayout, [RenderBox? child]) + : assert(onLayout != null), + super(child); + + ValueChanged onLayout; + + @override + Size computeDryLayout(BoxConstraints constraints) { + if (child == null) { + return Size.zero; + } + return child!.getDryLayout(constraints); + } + + @override + void performLayout() { + if (child == null) { + size = Size.zero; + } else { + child!.layout(constraints, parentUsesSize: true); + size = constraints.constrain(child!.size); + final BoxParentData childParentData = child!.parentData! as BoxParentData; + childParentData.offset = Offset.zero; + } + onLayout(size); + } +} + +/// An item in a material design popup menu. +/// +/// To show a popup menu, use the [showMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// To show a checkmark next to a popup menu item, consider using +/// [CheckedPopupMenuItem]. +/// +/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More +/// elaborate menus with icons can use a [ListTile]. By default, a +/// [PopupMenuItem] is [kMinInteractiveDimension] pixels high. If you use a widget +/// with a different height, it must be specified in the [height] property. +/// +/// {@tool snippet} +/// +/// Here, a [Text] widget is used with a popup menu item. The `Menu` type +/// is an enum, not shown here. +/// +/// ```dart +/// const PopupMenuItem( +/// value: Menu.itemOne, +/// child: Text('Item 1'), +/// ) +/// ``` +/// {@end-tool} +/// +/// See the example at [PopupMenuButton] for how this example could be used in a +/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to +/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child] +/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem] +/// that use a [ListTile] in their [child] slot. +/// +/// See also: +/// +/// * [PopupMenuDivider], which can be used to divide items from each other. +/// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class PopupMenuItem extends PopupMenuEntry { + /// Creates an item for a popup menu. + /// + /// By default, the item is [enabled]. + /// + /// The `enabled` and `height` arguments must not be null. + const PopupMenuItem({ + Key? key, + this.value, + this.onTap, + this.enabled = true, + this.height = kMinInteractiveDimension, + this.padding, + this.textStyle, + this.mouseCursor, + required this.child, + }) : assert(enabled != null), + assert(height != null), + super(key: key); + + /// The value that will be returned by [showMenu] if this entry is selected. + final T? value; + + /// Called when the menu item is tapped. + final VoidCallback? onTap; + + /// Whether the user is permitted to select this item. + /// + /// Defaults to true. If this is false, then the item will not react to + /// touches. + final bool enabled; + + /// The minimum height of the menu item. + /// + /// Defaults to [kMinInteractiveDimension] pixels. + @override + final double height; + + /// The padding of the menu item. + /// + /// Note that [height] may interact with the applied padding. For example, + /// If a [height] greater than the height of the sum of the padding and [child] + /// is provided, then the padding's effect will not be visible. + /// + /// When null, the horizontal padding defaults to 16.0 on both sides. + final EdgeInsets? padding; + + /// The text style of the popup menu item. + /// + /// If this property is null, then [PopupMenuThemeData.textStyle] is used. + /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1] + /// of [ThemeData.textTheme] is used. + final TextStyle? textStyle; + + /// {@template flutter.material.popupmenu.mouseCursor} + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [MaterialStateProperty], + /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: + /// + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// {@endtemplate} + /// + /// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If + /// that is also null, then [MaterialStateMouseCursor.clickable] is used. + final MouseCursor? mouseCursor; + + /// The widget below this widget in the tree. + /// + /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An + /// appropriate [DefaultTextStyle] is put in scope for the child. In either + /// case, the text should be short enough that it won't wrap. + final Widget? child; + + @override + bool represents(T? value) => value == this.value; + + @override + PopupMenuItemState> createState() => + PopupMenuItemState>(); +} + +/// The [State] for [PopupMenuItem] subclasses. +/// +/// By default this implements the basic styling and layout of Material Design +/// popup menu items. +/// +/// The [buildChild] method can be overridden to adjust exactly what gets placed +/// in the menu. By default it returns [PopupMenuItem.child]. +/// +/// The [handleTap] method can be overridden to adjust exactly what happens when +/// the item is tapped. By default, it uses [Navigator.pop] to return the +/// [PopupMenuItem.value] from the menu route. +/// +/// This class takes two type arguments. The second, `W`, is the exact type of +/// the [Widget] that is using this [State]. It must be a subclass of +/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget +/// class, and is the type of values returned from this menu. +class PopupMenuItemState> extends State { + /// The menu item contents. + /// + /// Used by the [build] method. + /// + /// By default, this returns [PopupMenuItem.child]. Override this to put + /// something else in the menu entry. + @protected + Widget? buildChild() => widget.child; + + /// The handler for when the user selects the menu item. + /// + /// Used by the [InkWell] inserted by the [build] method. + /// + /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from + /// the menu route. + @protected + void handleTap() { + widget.onTap?.call(); + + Navigator.pop(context, widget.value); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle style = widget.textStyle ?? + popupMenuTheme.textStyle ?? + theme.textTheme.subtitle1!; + + if (!widget.enabled) style = style.copyWith(color: theme.disabledColor); + + Widget item = AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), + child: buildChild(), + ), + ); + + if (!widget.enabled) { + final bool isDark = theme.brightness == Brightness.dark; + item = IconTheme.merge( + data: IconThemeData(opacity: isDark ? 0.5 : 0.38), + child: item, + ); + } + + return MergeSemantics( + child: Semantics( + enabled: widget.enabled, + button: true, + child: InkWell( + onTap: widget.enabled ? handleTap : null, + canRequestFocus: widget.enabled, + mouseCursor: _EffectiveMouseCursor( + widget.mouseCursor, popupMenuTheme.mouseCursor), + child: item, + ), + ), + ); + } +} + +/// An item with a checkmark in a material design popup menu. +/// +/// To show a popup menu, use the [showMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which +/// matches the default minimum height of a [PopupMenuItem]. The horizontal +/// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the +/// [ListTile.leading] position. +/// +/// {@tool snippet} +/// +/// Suppose a `Commands` enum exists that lists the possible commands from a +/// particular popup menu, including `Commands.heroAndScholar` and +/// `Commands.hurricaneCame`, and further suppose that there is a +/// `_heroAndScholar` member field which is a boolean. The example below shows a +/// menu with one menu item with a checkmark that can toggle the boolean, and +/// one menu item without a checkmark for selecting the second option. (It also +/// shows a divider placed between the two menu items.) +/// +/// ```dart +/// PopupMenuButton( +/// onSelected: (Commands result) { +/// switch (result) { +/// case Commands.heroAndScholar: +/// setState(() { _heroAndScholar = !_heroAndScholar; }); +/// break; +/// case Commands.hurricaneCame: +/// // ...handle hurricane option +/// break; +/// // ...other items handled here +/// } +/// }, +/// itemBuilder: (BuildContext context) => >[ +/// CheckedPopupMenuItem( +/// checked: _heroAndScholar, +/// value: Commands.heroAndScholar, +/// child: const Text('Hero and scholar'), +/// ), +/// const PopupMenuDivider(), +/// const PopupMenuItem( +/// value: Commands.hurricaneCame, +/// child: ListTile(leading: Icon(null), title: Text('Bring hurricane')), +/// ), +/// // ...other items listed here +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// In particular, observe how the second menu item uses a [ListTile] with a +/// blank [Icon] in the [ListTile.leading] position to get the same alignment as +/// the item with the checkmark. +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to +/// toggling a value). +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class CheckedPopupMenuItem extends PopupMenuItem { + /// Creates a popup menu item with a checkmark. + /// + /// By default, the menu item is [enabled] but unchecked. To mark the item as + /// checked, set [checked] to true. + /// + /// The `checked` and `enabled` arguments must not be null. + const CheckedPopupMenuItem({ + Key? key, + T? value, + this.checked = false, + bool enabled = true, + EdgeInsets? padding, + double height = kMinInteractiveDimension, + Widget? child, + }) : assert(checked != null), + super( + key: key, + value: value, + enabled: enabled, + padding: padding, + height: height, + child: child, + ); + + /// Whether to display a checkmark next to the menu item. + /// + /// Defaults to false. + /// + /// When true, an [Icons.done] checkmark is displayed. + /// + /// When this popup menu item is selected, the checkmark will fade in or out + /// as appropriate to represent the implied new state. + final bool checked; + + /// The widget below this widget in the tree. + /// + /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for + /// the child. The text should be short enough that it won't wrap. + /// + /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose + /// [ListTile.leading] slot is an [Icons.done] icon. + @override + Widget? get child => super.child; + + @override + PopupMenuItemState> createState() => + _CheckedPopupMenuItemState(); +} + +class _CheckedPopupMenuItemState + extends PopupMenuItemState> + with SingleTickerProviderStateMixin { + static const Duration _fadeDuration = Duration(milliseconds: 150); + late AnimationController _controller; + Animation get _opacity => _controller.view; + + @override + void initState() { + super.initState(); + _controller = AnimationController(duration: _fadeDuration, vsync: this) + ..value = widget.checked ? 1.0 : 0.0 + ..addListener(() => setState(() {/* animation changed */})); + } + + @override + void handleTap() { + // This fades the checkmark in or out when tapped. + if (widget.checked) + _controller.reverse(); + else + _controller.forward(); + super.handleTap(); + } + + @override + Widget buildChild() { + return ListTile( + enabled: widget.enabled, + leading: FadeTransition( + opacity: _opacity, + child: Icon(_controller.isDismissed ? null : Icons.done), + ), + title: widget.child, + ); + } +} + +class _PopupMenu extends StatelessWidget { + const _PopupMenu({ + Key? key, + required this.route, + required this.semanticLabel, + this.constraints, + }) : super(key: key); + + final _PopupMenuRoute route; + final String? semanticLabel; + final BoxConstraints? constraints; + + @override + Widget build(BuildContext context) { + final double unit = 1.0 / + (route.items.length + + 1.5); // 1.0 for the width and 0.5 for the last item's fade. + final List children = []; + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + + for (int i = 0; i < route.items.length; i += 1) { + final double start = (i + 1) * unit; + final double end = (start + 1.5 * unit).clamp(0.0, 1.0); + final CurvedAnimation opacity = CurvedAnimation( + parent: route.animation!, + curve: Interval(start, end), + ); + Widget item = route.items[i]; + if (route.initialValue != null && + route.items[i].represents(route.initialValue)) { + item = Container( + color: Theme.of(context).highlightColor, + child: item, + ); + } + children.add( + _MenuItem( + onLayout: (Size size) { + route.itemSizes[i] = size; + }, + child: FadeTransition( + opacity: opacity, + child: item, + ), + ), + ); + } + + final CurveTween opacity = + CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); + final CurveTween width = CurveTween(curve: Interval(0.0, unit)); + final CurveTween height = + CurveTween(curve: Interval(0.0, unit * route.items.length)); + + final Widget child = ConstrainedBox( + constraints: constraints ?? + const BoxConstraints( + minWidth: _kMenuMinWidth, + maxWidth: _kMenuMaxWidth, + ), + child: IntrinsicWidth( + stepWidth: _kMenuWidthStep, + child: Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: semanticLabel, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + vertical: _kMenuVerticalPadding, + ), + child: ListBody(children: children), + ), + ), + ), + ); + + return AnimatedBuilder( + animation: route.animation!, + builder: (BuildContext context, Widget? child) { + return FadeTransition( + opacity: opacity.animate(route.animation!), + child: Material( + shape: route.shape ?? popupMenuTheme.shape, + color: route.color ?? popupMenuTheme.color, + type: MaterialType.card, + elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0, + child: Align( + alignment: AlignmentDirectional.topEnd, + widthFactor: width.evaluate(route.animation!), + heightFactor: height.evaluate(route.animation!), + child: child, + ), + ), + ); + }, + child: child, + ); + } +} + +// Positioning of the menu on the screen. +class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { + _PopupMenuRouteLayout( + this.position, + this.itemSizes, + this.selectedItemIndex, + this.textDirection, + this.padding, + this.avoidBounds, + ); + + // Rectangle of underlying button, relative to the overlay's dimensions. + final RelativeRect position; + + // The sizes of each item are computed when the menu is laid out, and before + // the route is laid out. + List itemSizes; + + // The index of the selected item, or null if PopupMenuButton.initialValue + // was not specified. + final int? selectedItemIndex; + + // Whether to prefer going to the left or to the right. + final TextDirection textDirection; + + // The padding of unsafe area. + EdgeInsets padding; + + // List of rectangles that we should avoid overlapping. Unusable screen area. + final Set avoidBounds; + + // We put the child wherever position specifies, so long as it will fit within + // the specified parent size padded (inset) by 8. If necessary, we adjust the + // child's position so that it fits. + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + // The menu can be at most the size of the overlay minus 8.0 pixels in each + // direction. + return BoxConstraints.loose(constraints.biggest).deflate( + const EdgeInsets.all(_kMenuScreenPadding) + padding, + ); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + // size: The size of the overlay. + // childSize: The size of the menu, when fully open, as determined by + // getConstraintsForChild. + + final double buttonHeight = size.height - position.top - position.bottom; + // Find the ideal vertical position. + double y = position.top; + if (selectedItemIndex != null && itemSizes != null) { + double selectedItemOffset = _kMenuVerticalPadding; + for (int index = 0; index < selectedItemIndex!; index += 1) { + selectedItemOffset += itemSizes[index]!.height; + } + selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2; + y = y + buttonHeight / 2.0 - selectedItemOffset; + } + + // Find the ideal horizontal position. + double x; + // if (position.left > position.right) { + // // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. + // x = size.width - position.right - childSize.width; + // } else if (position.left < position.right) { + // // Menu button is closer to the left edge, so grow to the right, aligned to the left edge. + // x = position.left; + // } else { + // Menu button is equidistant from both edges, so grow in reading direction. + assert(textDirection != null); + switch (textDirection) { + case TextDirection.rtl: + x = size.width - position.right - childSize.width; + break; + case TextDirection.ltr: + x = position.left; + break; + } + //} + final Offset wantedPosition = Offset(x, y); + final Offset originCenter = position.toRect(Offset.zero & size).center; + final Iterable subScreens = + DisplayFeatureSubScreen.subScreensInBounds( + Offset.zero & size, avoidBounds); + final Rect subScreen = _closestScreen(subScreens, originCenter); + return _fitInsideScreen(subScreen, childSize, wantedPosition); + } + + Rect _closestScreen(Iterable screens, Offset point) { + Rect closest = screens.first; + for (final Rect screen in screens) { + if ((screen.center - point).distance < + (closest.center - point).distance) { + closest = screen; + } + } + return closest; + } + + Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition) { + double x = wantedPosition.dx; + double y = wantedPosition.dy; + // Avoid going outside an area defined as the rectangle 8.0 pixels from the + // edge of the screen in every direction. + if (x < screen.left + _kMenuScreenPadding + padding.left) { + x = screen.left + _kMenuScreenPadding + padding.left; + } else if (x + childSize.width > + screen.right - _kMenuScreenPadding - padding.right) { + x = screen.right - childSize.width - _kMenuScreenPadding - padding.right; + } + if (y < screen.top + _kMenuScreenPadding + padding.top) { + y = _kMenuScreenPadding + padding.top; + } else if (y + childSize.height > + screen.bottom - _kMenuScreenPadding - padding.bottom) { + y = screen.bottom - + childSize.height - + _kMenuScreenPadding - + padding.bottom; + } + + return Offset(x, y); + } + + @override + bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { + // If called when the old and new itemSizes have been initialized then + // we expect them to have the same length because there's no practical + // way to change length of the items list once the menu has been shown. + assert(itemSizes.length == oldDelegate.itemSizes.length); + + return position != oldDelegate.position || + selectedItemIndex != oldDelegate.selectedItemIndex || + textDirection != oldDelegate.textDirection || + !listEquals(itemSizes, oldDelegate.itemSizes) || + padding != oldDelegate.padding || + !setEquals(avoidBounds, oldDelegate.avoidBounds); + } +} + +class _PopupMenuRoute extends PopupRoute { + _PopupMenuRoute({ + required this.position, + required this.items, + this.initialValue, + this.elevation, + required this.barrierLabel, + this.semanticLabel, + this.shape, + this.color, + required this.capturedThemes, + this.constraints, + }) : itemSizes = List.filled(items.length, null); + + final RelativeRect position; + final List> items; + final List itemSizes; + final T? initialValue; + final double? elevation; + final String? semanticLabel; + final ShapeBorder? shape; + final Color? color; + final CapturedThemes capturedThemes; + final BoxConstraints? constraints; + + @override + Animation createAnimation() { + return CurvedAnimation( + parent: super.createAnimation(), + curve: Curves.linear, + reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd), + ); + } + + @override + Duration get transitionDuration => _kMenuDuration; + + @override + bool get barrierDismissible => true; + + @override + Color? get barrierColor => null; + + @override + final String barrierLabel; + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + int? selectedItemIndex; + if (initialValue != null) { + for (int index = 0; + selectedItemIndex == null && index < items.length; + index += 1) { + if (items[index].represents(initialValue)) selectedItemIndex = index; + } + } + + final Widget menu = _PopupMenu( + route: this, + semanticLabel: semanticLabel, + constraints: constraints, + ); + final MediaQueryData mediaQuery = MediaQuery.of(context); + return MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + removeLeft: true, + removeRight: true, + child: Builder( + builder: (BuildContext context) { + return CustomSingleChildLayout( + delegate: _PopupMenuRouteLayout( + position, + itemSizes, + selectedItemIndex, + Directionality.of(context), + mediaQuery.padding, + _avoidBounds(mediaQuery), + ), + child: capturedThemes.wrap(menu), + ); + }, + ), + ); + } + + Set _avoidBounds(MediaQueryData mediaQuery) { + return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet(); + } +} + +/// Show a popup menu that contains the `items` at `position`. +/// +/// `items` should be non-null and not empty. +/// +/// If `initialValue` is specified then the first item with a matching value +/// will be highlighted and the value of `position` gives the rectangle whose +/// vertical center will be aligned with the vertical center of the highlighted +/// item (when possible). +/// +/// If `initialValue` is not specified then the top of the menu will be aligned +/// with the top of the `position` rectangle. +/// +/// In both cases, the menu position will be adjusted if necessary to fit on the +/// screen. +/// +/// Horizontally, the menu is positioned so that it grows in the direction that +/// has the most room. For example, if the `position` describes a rectangle on +/// the left edge of the screen, then the left edge of the menu is aligned with +/// the left edge of the `position`, and the menu grows to the right. If both +/// edges of the `position` are equidistant from the opposite edge of the +/// screen, then the ambient [Directionality] is used as a tie-breaker, +/// preferring to grow in the reading direction. +/// +/// The positioning of the `initialValue` at the `position` is implemented by +/// iterating over the `items` to find the first whose +/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then +/// summing the values of [PopupMenuEntry.height] for all the preceding widgets +/// in the list. +/// +/// The `elevation` argument specifies the z-coordinate at which to place the +/// menu. The elevation defaults to 8, the appropriate elevation for popup +/// menus. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the menu. It is only used when the method is called. Its corresponding +/// widget can be safely removed from the tree before the popup menu is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// menu to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. +/// +/// The `semanticLabel` argument is used by accessibility frameworks to +/// announce screen transitions when the menu is opened and closed. If this +/// label is not provided, it will default to +/// [MaterialLocalizations.popupMenuLabel]. +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by +/// calling this method automatically. +/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered +/// semantics. +Future showMenu({ + required BuildContext context, + required RelativeRect position, + required List> items, + T? initialValue, + double? elevation, + String? semanticLabel, + ShapeBorder? shape, + Color? color, + bool useRootNavigator = false, + BoxConstraints? constraints, +}) { + assert(context != null); + assert(position != null); + assert(useRootNavigator != null); + assert(items != null && items.isNotEmpty); + assert(debugCheckHasMaterialLocalizations(context)); + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel; + } + + final NavigatorState navigator = + Navigator.of(context, rootNavigator: useRootNavigator); + return navigator.push(_PopupMenuRoute( + position: position, + items: items, + initialValue: initialValue, + elevation: elevation, + semanticLabel: semanticLabel, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + shape: shape, + color: color, + capturedThemes: + InheritedTheme.capture(from: context, to: navigator.context), + constraints: constraints, + )); +} + +/// Signature for the callback invoked when a menu item is selected. The +/// argument is the value of the [PopupMenuItem] that caused its menu to be +/// dismissed. +/// +/// Used by [PopupMenuButton.onSelected]. +typedef PopupMenuItemSelected = void Function(T value); + +/// Signature for the callback invoked when a [PopupMenuButton] is dismissed +/// without selecting an item. +/// +/// Used by [PopupMenuButton.onCanceled]. +typedef PopupMenuCanceled = void Function(); + +/// Signature used by [PopupMenuButton] to lazily construct the items shown when +/// the button is pressed. +/// +/// Used by [PopupMenuButton.itemBuilder]. +typedef PopupMenuItemBuilder = List> Function( + BuildContext context); + +/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed +/// because an item was selected. The value passed to [onSelected] is the value of +/// the selected menu item. +/// +/// One of [child] or [icon] may be provided, but not both. If [icon] is provided, +/// then [PopupMenuButton] behaves like an [IconButton]. +/// +/// If both are null, then a standard overflow icon is created (depending on the +/// platform). +/// +/// {@tool dartpad} +/// This example shows a menu with four items, selecting between an enum's +/// values and setting a `_selectedMenu` field based on the selection +/// +/// ** See code in examples/api/lib/material/popupmenu/popupmenu.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +class PopupMenuButton extends StatefulWidget { + /// Creates a button that shows a popup menu. + /// + /// The [itemBuilder] argument must not be null. + const PopupMenuButton({ + Key? key, + required this.itemBuilder, + this.initialValue, + this.onSelected, + this.onCanceled, + this.tooltip, + this.elevation, + this.padding = const EdgeInsets.all(8.0), + this.child, + this.splashRadius, + this.icon, + this.iconSize, + this.offset = Offset.zero, + this.enabled = true, + this.shape, + this.color, + this.enableFeedback, + this.constraints, + this.position = PopupMenuPosition.over, + }) : assert(itemBuilder != null), + assert(enabled != null), + assert( + !(child != null && icon != null), + 'You can only pass [child] or [icon], not both.', + ), + super(key: key); + + /// Called when the button is pressed to create the items to show in the menu. + final PopupMenuItemBuilder itemBuilder; + + /// The value of the menu item, if any, that should be highlighted when the menu opens. + final T? initialValue; + + /// Called when the user selects a value from the popup menu created by this button. + /// + /// If the popup menu is dismissed without selecting a value, [onCanceled] is + /// called instead. + final PopupMenuItemSelected? onSelected; + + /// Called when the user dismisses the popup menu without selecting an item. + /// + /// If the user selects a value, [onSelected] is called instead. + final PopupMenuCanceled? onCanceled; + + /// Text that describes the action that will occur when the button is pressed. + /// + /// This text is displayed when the user long-presses on the button and is + /// used for accessibility. + final String? tooltip; + + /// The z-coordinate at which to place the menu when open. This controls the + /// size of the shadow below the menu. + /// + /// Defaults to 8, the appropriate elevation for popup menus. + final double? elevation; + + /// Matches IconButton's 8 dps padding by default. In some cases, notably where + /// this button appears as the trailing element of a list item, it's useful to be able + /// to set the padding to zero. + final EdgeInsetsGeometry padding; + + /// The splash radius. + /// + /// If null, default splash radius of [InkWell] or [IconButton] is used. + final double? splashRadius; + + /// If provided, [child] is the widget used for this button + /// and the button will utilize an [InkWell] for taps. + final Widget? child; + + /// If provided, the [icon] is used for this button + /// and the button will behave like an [IconButton]. + final Widget? icon; + + /// The offset is applied relative to the initial position + /// set by the [position]. + /// + /// When not set, the offset defaults to [Offset.zero]. + final Offset offset; + + /// Whether this popup menu button is interactive. + /// + /// Must be non-null, defaults to `true` + /// + /// If `true` the button will respond to presses by displaying the menu. + /// + /// If `false`, the button is styled with the disabled color from the + /// current [Theme] and will not respond to presses or show the popup + /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called. + /// + /// This can be useful in situations where the app needs to show the button, + /// but doesn't currently have anything to show in the menu. + final bool enabled; + + /// If provided, the shape used for the menu. + /// + /// If this property is null, then [PopupMenuThemeData.shape] is used. + /// If [PopupMenuThemeData.shape] is also null, then the default shape for + /// [MaterialType.card] is used. This default shape is a rectangle with + /// rounded edges of BorderRadius.circular(2.0). + final ShapeBorder? shape; + + /// If provided, the background color used for the menu. + /// + /// If this property is null, then [PopupMenuThemeData.color] is used. + /// If [PopupMenuThemeData.color] is also null, then + /// Theme.of(context).cardColor is used. + final Color? color; + + /// Whether detected gestures should provide acoustic and/or haptic feedback. + /// + /// For example, on Android a tap will produce a clicking sound and a + /// long-press will produce a short vibration, when feedback is enabled. + /// + /// See also: + /// + /// * [Feedback] for providing platform-specific feedback to certain actions. + final bool? enableFeedback; + + /// If provided, the size of the [Icon]. + /// + /// If this property is null, then [IconThemeData.size] is used. + /// If [IconThemeData.size] is also null, then + /// default size is 24.0 pixels. + final double? iconSize; + + /// Optional size constraints for the menu. + /// + /// When unspecified, defaults to: + /// ```dart + /// const BoxConstraints( + /// minWidth: 2.0 * 56.0, + /// maxWidth: 5.0 * 56.0, + /// ) + /// ``` + /// + /// The default constraints ensure that the menu width matches maximum width + /// recommended by the material design guidelines. + /// Specifying this parameter enables creation of menu wider than + /// the default maximum width. + final BoxConstraints? constraints; + + /// Whether the popup menu is positioned over or under the popup menu button. + /// + /// [offset] is used to change the position of the popup menu relative to the + /// position set by this parameter. + /// + /// When not set, the position defaults to [PopupMenuPosition.over] which makes the + /// popup menu appear directly over the button that was used to create it. + final PopupMenuPosition position; + + @override + PopupMenuButtonState createState() => PopupMenuButtonState(); +} + +/// The [State] for a [PopupMenuButton]. +/// +/// See [showButtonMenu] for a way to programmatically open the popup menu +/// of your button state. +class PopupMenuButtonState extends State> { + /// A method to show a popup menu with the items supplied to + /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton]. + /// + /// By default, it is called when the user taps the button and [PopupMenuButton.enabled] + /// is set to `true`. Moreover, you can open the button by calling the method manually. + /// + /// You would access your [PopupMenuButtonState] using a [GlobalKey] and + /// show the menu of the button with `globalKey.currentState.showButtonMenu`. + void showButtonMenu() { + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + final RenderBox button = context.findRenderObject()! as RenderBox; + final RenderBox overlay = + Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; + final Offset offset; + switch (widget.position) { + case PopupMenuPosition.over: + offset = widget.offset; + break; + case PopupMenuPosition.under: + offset = + Offset(0.0, button.size.height - (widget.padding.vertical / 2)) + + widget.offset; + break; + case PopupMenuPosition.overSide: + offset = + Offset(button.size.width - (widget.padding.horizontal / 2), 0.0) + + widget.offset; + break; + case PopupMenuPosition.underSide: + offset = Offset(button.size.width - (widget.padding.horizontal / 2), + button.size.height - (widget.padding.vertical / 2)) + + widget.offset; + break; + } + final RelativeRect position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(offset, ancestor: overlay), + button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, + ancestor: overlay), + ), + Offset.zero & overlay.size, + ); + final List> items = widget.itemBuilder(context); + // Only show the menu if there is something to show + if (items.isNotEmpty) { + showMenu( + context: context, + elevation: widget.elevation ?? popupMenuTheme.elevation, + items: items, + initialValue: widget.initialValue, + position: position, + shape: widget.shape ?? popupMenuTheme.shape, + color: widget.color ?? popupMenuTheme.color, + constraints: widget.constraints, + ).then((T? newValue) { + if (!mounted) return null; + if (newValue == null) { + widget.onCanceled?.call(); + return null; + } + widget.onSelected?.call(newValue); + }); + } + } + + bool get _canRequestFocus { + final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? + NavigationMode.traditional; + switch (mode) { + case NavigationMode.traditional: + return widget.enabled; + case NavigationMode.directional: + return true; + } + } + + @override + Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + final bool enableFeedback = widget.enableFeedback ?? + PopupMenuTheme.of(context).enableFeedback ?? + true; + + assert(debugCheckHasMaterialLocalizations(context)); + + if (widget.child != null) + return Tooltip( + message: + widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, + child: InkWell( + onTap: widget.enabled ? showButtonMenu : null, + canRequestFocus: _canRequestFocus, + radius: widget.splashRadius, + enableFeedback: enableFeedback, + child: widget.child, + ), + ); + + return IconButton( + icon: widget.icon ?? Icon(Icons.adaptive.more), + padding: widget.padding, + splashRadius: widget.splashRadius, + iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, + tooltip: + widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, + onPressed: widget.enabled ? showButtonMenu : null, + enableFeedback: enableFeedback, + ); + } +} + +// This MaterialStateProperty is passed along to the menu item's InkWell which +// resolves the property against MaterialState.disabled, MaterialState.hovered, +// MaterialState.focused. +class _EffectiveMouseCursor extends MaterialStateMouseCursor { + const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor); + + final MouseCursor? widgetCursor; + final MaterialStateProperty? themeCursor; + + @override + MouseCursor resolve(Set states) { + return MaterialStateProperty.resolveAs( + widgetCursor, states) ?? + themeCursor?.resolve(states) ?? + MaterialStateMouseCursor.clickable.resolve(states); + } + + @override + String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)'; +} diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 3bfff60bf..02b5b9f00 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -21,18 +21,16 @@ final peerSearchTextController = TextEditingController(text: peerSearchText.value); class _PeerWidget extends StatefulWidget { - late final _peers; - late final OffstageFunc _offstageFunc; - late final PeerCardWidgetFunc _peerCardWidgetFunc; + final Peers peers; + final OffstageFunc offstageFunc; + final PeerCardWidgetFunc peerCardWidgetFunc; - _PeerWidget(Peers peers, OffstageFunc offstageFunc, - PeerCardWidgetFunc peerCardWidgetFunc, - {Key? key}) - : super(key: key) { - _peers = peers; - _offstageFunc = offstageFunc; - _peerCardWidgetFunc = peerCardWidgetFunc; - } + const _PeerWidget( + {required this.peers, + required this.offstageFunc, + required this.peerCardWidgetFunc, + Key? key}) + : super(key: key); @override _PeerWidgetState createState() => _PeerWidgetState(); @@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget { class _PeerWidgetState extends State<_PeerWidget> with WindowListener { static const int _maxQueryCount = 3; - var _curPeers = Set(); + final _curPeers = {}; var _lastChangeTime = DateTime.now(); - var _lastQueryPeers = Set(); + var _lastQueryPeers = {}; var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); var _queryCoun = 0; var _exit = false; @@ -78,65 +76,62 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { @override Widget build(BuildContext context) { - final space = 12.0; + const space = 12.0; return ChangeNotifierProvider( - create: (context) => super.widget._peers, + create: (context) => widget.peers, child: Consumer( - builder: (context, peers, child) => peers.peers.isEmpty - ? Center( - child: Text(translate("Empty")), - ) - : SingleChildScrollView( - child: ObxValue((searchText) { - return FutureBuilder>( - builder: (context, snapshot) { - if (snapshot.hasData) { - final peers = snapshot.data!; - final cards = []; - for (final peer in peers) { - cards.add(Offstage( - key: ValueKey("off${peer.id}"), - offstage: super.widget._offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: super - .widget - ._peerCardWidgetFunc(peer), - ), + builder: (context, peers, child) => peers.peers.isEmpty + ? Center( + child: Text(translate("Empty")), + ) + : SingleChildScrollView( + child: ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: widget.offstageFunc(peer), + child: Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = + (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), ), - ))); - } - return Wrap( - spacing: space, - runSpacing: space, - children: cards); - } else { - return const Center( - child: CircularProgressIndicator(), - ); + ), + ))); } - }, - future: matchPeers(searchText.value, peers.peers), - ); - }, peerSearchText), - )), + return Wrap( + spacing: space, runSpacing: space, children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText), + ), + ), ); } @@ -175,31 +170,42 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } abstract class BasePeerWidget extends StatelessWidget { - late final _name; - late final _loadEvent; - late final OffstageFunc _offstageFunc; - late final PeerCardWidgetFunc _peerCardWidgetFunc; - late final List _initPeers; + final String name; + final String loadEvent; + final OffstageFunc offstageFunc; + final PeerCardWidgetFunc peerCardWidgetFunc; + final List initPeers; - BasePeerWidget({Key? key}) : super(key: key) {} + const BasePeerWidget({ + Key? key, + required this.name, + required this.loadEvent, + required this.offstageFunc, + required this.peerCardWidgetFunc, + required this.initPeers, + }) : super(key: key); @override Widget build(BuildContext context) { - return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc, - _peerCardWidgetFunc); + return _PeerWidget( + peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), + offstageFunc: offstageFunc, + peerCardWidgetFunc: peerCardWidgetFunc); } } class RecentPeerWidget extends BasePeerWidget { - RecentPeerWidget({Key? key}) : super(key: key) { - super._name = "recent peer"; - super._loadEvent = "load_recent_peers"; - super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard( - peer: peer, + RecentPeerWidget({Key? key}) + : super( + key: key, + name: 'recent peer', + loadEvent: 'load_recent_peers', + offstageFunc: (Peer peer) => false, + peerCardWidgetFunc: (Peer peer) => RecentPeerCard( + peer: peer, + ), + initPeers: [], ); - super._initPeers = []; - } @override Widget build(BuildContext context) { @@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget { } class FavoritePeerWidget extends BasePeerWidget { - FavoritePeerWidget({Key? key}) : super(key: key) { - super._name = "favorite peer"; - super._loadEvent = "load_fav_peers"; - super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); - super._initPeers = []; - } + FavoritePeerWidget({Key? key}) + : super( + key: key, + name: 'favorite peer', + loadEvent: 'load_fav_peers', + offstageFunc: (Peer peer) => false, + peerCardWidgetFunc: (Peer peer) => FavoritePeerCard( + peer: peer, + ), + initPeers: [], + ); @override Widget build(BuildContext context) { @@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget { } class DiscoveredPeerWidget extends BasePeerWidget { - DiscoveredPeerWidget({Key? key}) : super(key: key) { - super._name = "discovered peer"; - super._loadEvent = "load_lan_peers"; - super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); - super._initPeers = []; - } + DiscoveredPeerWidget({Key? key}) + : super( + key: key, + name: 'discovered peer', + loadEvent: 'load_lan_peers', + offstageFunc: (Peer peer) => false, + peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peer: peer, + ), + initPeers: [], + ); @override Widget build(BuildContext context) { @@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget { } class AddressBookPeerWidget extends BasePeerWidget { - AddressBookPeerWidget({Key? key}) : super(key: key) { - super._name = "address book peer"; - super._offstageFunc = - (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); - super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); - super._initPeers = _loadPeers(); - } + AddressBookPeerWidget({Key? key}) + : super( + key: key, + name: 'address book peer', + loadEvent: 'load_address_book_peers', + offstageFunc: (Peer peer) => + !_hitTag(gFFI.abModel.selectedTags, peer.tags), + peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peer: peer, + ), + initPeers: _loadPeers(), + ); - List _loadPeers() { + static List _loadPeers() { return gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); } - bool _hitTag(List selectedTags, List idents) { + static bool _hitTag(List selectedTags, List idents) { if (selectedTags.isEmpty) { return true; } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 433ca9284..114f4146e 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -1,5 +1,6 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -7,10 +8,18 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; +import './material_mod_popup_menu.dart' as mod_menu; +import './popup_menu.dart'; -typedef PopupMenuItemsFunc = Future>> Function(); +class _PopupMenuTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 25.0; + static const double dividerHeight = 12.0; +} -enum PeerType { recent, fav, discovered, ab } +typedef PopupMenuEntryBuilder = Future>> + Function(BuildContext); enum PeerUiType { grid, list } @@ -18,14 +27,16 @@ final peerCardUiType = PeerUiType.grid.obs; class _PeerCard extends StatefulWidget { final Peer peer; - final PopupMenuItemsFunc popupMenuItemsFunc; - final PeerType type; + final RxString alias; + final Function(BuildContext, String) connect; + final PopupMenuEntryBuilder popupMenuEntryBuilder; _PeerCard( {required this.peer, - required this.popupMenuItemsFunc, - Key? key, - required this.type}) + required this.alias, + required this.connect, + required this.popupMenuEntryBuilder, + Key? key}) : super(key: key); @override @@ -35,7 +46,6 @@ class _PeerCard extends StatefulWidget { /// State for the connection page. class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { - var _menuPos = RelativeRect.fill; final double _cardRadis = 20; final double _borderWidth = 2; final RxBool _iconMoreHover = false.obs; @@ -65,7 +75,7 @@ class _PeerCardState extends State<_PeerCard> : null); }, child: GestureDetector( - onDoubleTap: () => _connect(peer.id), + onDoubleTap: () => widget.connect(context, peer.id), child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), @@ -184,46 +194,28 @@ class _PeerCardState extends State<_PeerCard> children: [ Container( padding: const EdgeInsets.all(6), - child: - _getPlatformImage('${peer.platform}', 60), + child: _getPlatformImage(peer.platform, 60), ), Row( children: [ Expanded( - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - waitDuration: Duration(seconds: 1), - child: Text( - name, - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Center( - child: Text( - '${peer.username}@${peer.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )); - } - }, - ), + child: Obx(() { + final name = widget.alias.value.isEmpty + ? '${peer.username}@${peer.hostname}' + : widget.alias.value; + return Tooltip( + message: name, + waitDuration: Duration(seconds: 1), + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + }), ), ], ), @@ -247,7 +239,7 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text('${peer.id}') + Text(peer.id) ]).paddingSymmetric(vertical: 8), _actionMore(peer), ], @@ -261,36 +253,106 @@ class _PeerCardState extends State<_PeerCard> ); } - Widget _actionMore(Peer peer) => Listener( - onPointerDown: (e) { - final x = e.position.dx; - final y = e.position.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onPointerUp: (_) => _showPeerMenu(context, peer.id), - child: MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, - child: CircleAvatar( - radius: 14, - backgroundColor: _iconMoreHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - child: Icon(Icons.more_vert, - size: 18, - color: _iconMoreHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText)))); + Widget _actionMore(Peer peer) { + return FutureBuilder( + future: widget.popupMenuEntryBuilder(context), + initialData: const >[], + builder: (BuildContext context, + AsyncSnapshot>> snapshot) { + if (snapshot.hasData) { + return Listener( + child: MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => snapshot.data!, + )))); + } else { + return Container(); + } + }); + } + + /// Get the image for the current [platform]. + Widget _getPlatformImage(String platform, double size) { + platform = platform.toLowerCase(); + if (platform == 'mac os') { + platform = 'mac'; + } else if (platform != 'linux' && platform != 'android') { + platform = 'win'; + } + return Image.asset('assets/$platform.png', height: size, width: size); + } + + @override + bool get wantKeepAlive => true; +} + +abstract class BasePeerCard extends StatelessWidget { + final RxString alias = ''.obs; + final Peer peer; + + BasePeerCard({required this.peer, Key? key}) : super(key: key) { + bind + .mainGetPeerOption(id: peer.id, key: 'alias') + .then((value) => alias.value = value); + } + + @override + Widget build(BuildContext context) { + return _PeerCard( + peer: peer, + alias: alias, + connect: (BuildContext context, String id) => _connect(context, id), + popupMenuEntryBuilder: _buildPopupMenuEntry, + ); + } + + Future>> _buildPopupMenuEntry( + BuildContext context) async => + (await _buildMenuItems(context)) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: _PopupMenuTheme.commonColor, + height: _PopupMenuTheme.height, + dividerHeight: _PopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(); + + @protected + Future>> _buildMenuItems(BuildContext context); /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. - void _connect(String id, {bool isFileTransfer = false}) async { + /// If [isTcpTunneling], starts a session only for tcp tunneling. + /// If [isRDP], starts a session only for rdp. + void _connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); + assert(!(isFileTransfer && isTcpTunneling && isRDP), + "more than one connect type"); if (isFileTransfer) { - await rustDeskWinManager.new_file_transfer(id); + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); } else { - await rustDeskWinManager.new_remote_desktop(id); + await rustDeskWinManager.newRemoteDesktop(id); } FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { @@ -298,101 +360,377 @@ class _PeerCardState extends State<_PeerCard> } } - /// Show the peer menu and handle user's choice. - /// User might remove the peer or send a file to the peer. - void _showPeerMenu(BuildContext context, String id) async { - var value = await showMenu( - context: context, - position: _menuPos, - items: await super.widget.popupMenuItemsFunc(), - elevation: 8, - ); - if (value == 'remove') { - await bind.mainRemovePeer(id: id); - removePreference(id); - Get.forceAppUpdate(); // TODO use inner model / state - } else if (value == 'file') { - _connect(id, isFileTransfer: true); - } else if (value == 'add-fav') { - final favs = (await bind.mainGetFav()).toList(); - if (favs.indexOf(id) < 0) { - favs.add(id); - bind.mainStoreFav(favs: favs); - } - } else if (value == 'remove-fav') { - final favs = (await bind.mainGetFav()).toList(); - if (favs.remove(id)) { - bind.mainStoreFav(favs: favs); - Get.forceAppUpdate(); // TODO use inner model / state - } - } else if (value == 'connect') { - _connect(id, isFileTransfer: false); - } else if (value == 'ab-delete') { - gFFI.abModel.deletePeer(id); - await gFFI.abModel.updateAb(); - setState(() {}); - } else if (value == 'ab-edit-tag') { - _abEditTag(id); - } else if (value == 'rename') { - _rename(id); - } else if (value == 'unremember-password') { - await bind.mainForgetPassword(id: id); - } else if (value == 'force-always-relay') { - String value; - String oldValue = - await bind.mainGetPeerOption(id: id, key: 'force-always-relay'); - if (oldValue.isEmpty) { - value = 'Y'; - } else { - value = ''; - } - await bind.mainSetPeerOption( - id: id, key: 'force-always-relay', value: value); - } - } - - Widget _buildTag(String tagName, RxList rxTags, - {Function()? onTap}) { - return ContextMenuArea( - width: 100, - builder: (context) => [ - ListTile( - title: Text(translate("Delete")), - onTap: () { - gFFI.abModel.deleteTag(tagName); - gFFI.abModel.updateAb(); - Future.delayed(Duration.zero, () => Get.back()); - }, - ) - ], - child: GestureDetector( - onTap: onTap, - child: Obx( - () => Container( - decoration: BoxDecoration( - color: rxTags.contains(tagName) ? Colors.blue : null, - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text( - tagName, - style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), - ), - ), - ), + MenuEntryBase _connectCommonAction( + BuildContext context, String id, String title, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate(title), + style: style, ), + proc: () { + _connect( + context, + peer.id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + ); + }, + dismissOnClicked: true, ); } - /// Get the image for the current [platform]. - Widget _getPlatformImage(String platform, double size) { - platform = platform.toLowerCase(); - if (platform == 'mac os') - platform = 'mac'; - else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', height: size, width: size); + @protected + MenuEntryBase _connectAction(BuildContext context, String id) { + return _connectCommonAction(context, id, 'Connect'); + } + + @protected + MenuEntryBase _transferFileAction(BuildContext context, String id) { + return _connectCommonAction( + context, + id, + 'Transfer File', + isFileTransfer: true, + ); + } + + @protected + MenuEntryBase _tcpTunnelingAction(BuildContext context, String id) { + return _connectCommonAction( + context, + id, + 'TCP Tunneling', + isTcpTunneling: true, + ); + } + + @protected + MenuEntryBase _rdpAction(BuildContext context, String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('RDP'), + style: style, + ), + SizedBox(width: 20), + IconButton( + icon: Icon(Icons.edit), + onPressed: () => _rdpDialog(id), + ) + ], + ), + proc: () { + _connect(context, id, isRDP: true); + }, + dismissOnClicked: true, + ); + } + + @protected + Future> _forceAlwaysRelayAction(String id) async { + const option = 'force-always-relay'; + return MenuEntrySwitch( + text: translate('Always connect via relay'), + getter: () async { + return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; + }, + setter: (bool v) async { + String value; + String oldValue = await bind.mainGetPeerOption(id: id, key: option); + if (oldValue.isEmpty) { + value = 'Y'; + } else { + value = ''; + } + await bind.mainSetPeerOption(id: id, key: option, value: value); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _renameAction(String id, bool isAddressBook) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Rename'), + style: style, + ), + proc: () { + _rename(id, isAddressBook); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove'), + style: style, + ), + proc: () { + () async { + await bind.mainRemovePeer(id: id); + removePreference(id); + await reloadFunc(); + // Get.forceAppUpdate(); // TODO use inner model / state + }(); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _unrememberPasswordAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Unremember Password'), + style: style, + ), + proc: () { + bind.mainForgetPassword(id: id); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _addFavAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Add to Favorites'), + style: style, + ), + proc: () { + () async { + final favs = (await bind.mainGetFav()).toList(); + if (!favs.contains(id)) { + favs.add(id); + bind.mainStoreFav(favs: favs); + } + }(); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _rmFavAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove from Favorites'), + style: style, + ), + proc: () { + () async { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + bind.mainStoreFav(favs: favs); + Get.forceAppUpdate(); // TODO use inner model / state + } + }(); + }, + dismissOnClicked: true, + ); + } + + void _rename(String id, bool isAddressBook) async { + RxBool isInProgress = false.obs; + var name = await bind.mainGetPeerOption(id: id, key: 'alias'); + var controller = TextEditingController(text: name); + if (isAddressBook) { + final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); + if (peer == null) { + // this should not happen + } else { + name = peer['alias'] ?? ''; + } + } + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate('Rename')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Form( + child: TextFormField( + controller: controller, + decoration: InputDecoration(border: OutlineInputBorder()), + ), + ), + ), + Obx(() => Offstage( + offstage: isInProgress.isFalse, + child: LinearProgressIndicator())), + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + isInProgress.value = true; + name = controller.text; + await bind.mainSetPeerOption(id: id, key: 'alias', value: name); + if (isAddressBook) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } + alias.value = + await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + close(); + isInProgress.value = false; + }, + child: Text(translate("OK"))), + ], + ); + }); + } +} + +class RecentPeerCard extends BasePeerCard { + RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(peer.id)); + return menuItems; + } +} + +class FavoritePeerCard extends BasePeerCard { + FavoritePeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_rmFavAction(peer.id)); + return menuItems; + } +} + +class DiscoveredPeerCard extends BasePeerCard { + DiscoveredPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + })); + menuItems.add(_unrememberPasswordAction(peer.id)); + return menuItems; + } +} + +class AddressBookPeerCard extends BasePeerCard { + AddressBookPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id, () async {})); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(peer.id)); + menuItems.add(_editTagAction(peer.id)); + return menuItems; + } + + @protected + @override + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove'), + style: style, + ), + proc: () { + () async { + gFFI.abModel.deletePeer(id); + await gFFI.abModel.updateAb(); + }(); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _editTagAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Edit Tag'), + style: style, + ), + proc: () { + _abEditTag(id); + }, + dismissOnClicked: true, + ); } void _abEditTag(String id) { @@ -445,207 +783,40 @@ class _PeerCardState extends State<_PeerCard> }); } - void _rename(String id) async { - var isInProgress = false; - 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) { - // this should not happen - } else { - name = peer['alias'] ?? ""; - } - } - final k = GlobalKey(); - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate("Rename")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Form( - key: k, - child: TextFormField( - controller: TextEditingController(text: name), - decoration: InputDecoration(border: OutlineInputBorder()), - onChanged: (newStr) { - name = newStr; - }, - validator: (s) { - if (s == null || s.isEmpty) { - return translate("Empty"); - } - return null; - }, - onSaved: (s) { - name = s ?? "unnamed"; - }, - ), - ), + Widget _buildTag(String tagName, RxList rxTags, + {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) - ], + ), ), - actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - isInProgress = true; - }); - if (k.currentState != null) { - if (k.currentState!.validate()) { - k.currentState!.save(); - await bind.mainSetPeerOption( - id: id, key: 'alias', value: name); - if (widget.type == PeerType.ab) { - gFFI.abModel.setPeerOption(id, 'alias', name); - await gFFI.abModel.updateAb(); - } else { - Future.delayed(Duration.zero, () { - this.setState(() {}); - }); - } - close(); - } - } - setState(() { - isInProgress = false; - }); - }, - child: Text(translate("OK"))), - ], - ); - }); - } - - @override - bool get wantKeepAlive => true; -} - -abstract class BasePeerCard extends StatelessWidget { - final Peer peer; - final PeerType type; - - BasePeerCard({required this.peer, required this.type, Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return _PeerCard( - peer: peer, - popupMenuItemsFunc: _getPopupMenuItems, - type: type, + ), ); } - - @protected - Future>> _getPopupMenuItems(); -} - -class RecentPeerCard extends BasePeerCard { - RecentPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.recent); - - Future>> _getPopupMenuItems() async { - return [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav'), - ]; - } -} - -class FavoritePeerCard extends BasePeerCard { - FavoritePeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.fav); - - Future>> _getPopupMenuItems() async { - return [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Remove from Favorites')), value: 'remove-fav'), - ]; - } -} - -class DiscoveredPeerCard extends BasePeerCard { - DiscoveredPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.discovered); - - Future>> _getPopupMenuItems() async { - return [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav'), - ]; - } -} - -class AddressBookPeerCard extends BasePeerCard { - AddressBookPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.ab); - - Future>> _getPopupMenuItems() async { - return [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem( - child: Text(translate('Remove')), value: 'ab-delete'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav'), - PopupMenuItem( - child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), - ]; - } } Future> _forceAlwaysRelayMenuItem(String id) async { @@ -664,3 +835,136 @@ Future> _forceAlwaysRelayMenuItem(String id) async { ), value: 'force-always-relay'); } + +PopupMenuItem _rdpMenuItem(String id) { + return PopupMenuItem( + child: Row( + children: [ + Text('RDP'), + SizedBox(width: 20), + IconButton( + icon: Icon(Icons.edit), + onPressed: () => _rdpDialog(id), + ) + ], + ), + value: 'RDP'); +} + +void _rdpDialog(String id) async { + final portController = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_port')); + final userController = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_username')); + final passwordContorller = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); + RxBool secure = true.obs; + + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text('RDP ' + translate('Settings')), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Port')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')) + ], + decoration: InputDecoration( + border: OutlineInputBorder(), hintText: '3389'), + controller: portController, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + decoration: InputDecoration(border: OutlineInputBorder()), + controller: userController, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: Obx(() => TextField( + obscureText: secure.value, + decoration: InputDecoration( + border: OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => secure.value = !secure.value, + icon: Icon(secure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: passwordContorller, + )), + ), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + await bind.mainSetPeerOption( + id: id, key: 'rdp_port', value: portController.text.trim()); + await bind.mainSetPeerOption( + id: id, key: 'rdp_username', value: userController.text); + await bind.mainSetPeerOption( + id: id, key: 'rdp_password', value: passwordContorller.text); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart new file mode 100644 index 000000000..45e52cf81 --- /dev/null +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -0,0 +1,509 @@ +import 'dart:core'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import './material_mod_popup_menu.dart' as mod_menu; + +// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu +class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { + const PopupMenuChildrenItem({ + key, + this.height = kMinInteractiveDimension, + this.padding, + this.enable = true, + this.textStyle, + this.onTap, + this.position = mod_menu.PopupMenuPosition.overSide, + this.offset = Offset.zero, + required this.itemBuilder, + required this.child, + }) : super(key: key); + + final mod_menu.PopupMenuPosition position; + final Offset offset; + final TextStyle? textStyle; + final EdgeInsets? padding; + final bool enable; + final void Function()? onTap; + final List> Function(BuildContext) itemBuilder; + final Widget child; + + @override + final double height; + + @override + bool represents(T? value) => false; + + @override + MyPopupMenuItemState> createState() => + MyPopupMenuItemState>(); +} + +class MyPopupMenuItemState> + extends State { + @protected + void handleTap(T value) { + widget.onTap?.call(); + Navigator.pop(context, value); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle style = widget.textStyle ?? + popupMenuTheme.textStyle ?? + theme.textTheme.subtitle1!; + + return mod_menu.PopupMenuButton( + enabled: widget.enable, + position: widget.position, + offset: widget.offset, + onSelected: handleTap, + itemBuilder: widget.itemBuilder, + padding: EdgeInsets.zero, + child: AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: widget.child, + ), + ), + ); + } +} + +class MenuConfig { + // adapt to the screen height + static const fontSize = 14.0; + static const midPadding = 10.0; + static const iconScale = 0.8; + static const iconWidth = 12.0; + static const iconHeight = 12.0; + + final double height; + final double dividerHeight; + final Color commonColor; + + const MenuConfig( + {required this.commonColor, + this.height = kMinInteractiveDimension, + this.dividerHeight = 16.0}); +} + +abstract class MenuEntryBase { + bool dismissOnClicked; + + MenuEntryBase({this.dismissOnClicked = false}); + List> build(BuildContext context, MenuConfig conf); +} + +class MenuEntryDivider extends MenuEntryBase { + @override + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuDivider( + height: conf.dividerHeight, + ) + ]; + } +} + +class MenuEntryRadioOption { + String text; + String value; + bool dismissOnClicked; + + MenuEntryRadioOption( + {required this.text, required this.value, this.dismissOnClicked = false}); +} + +typedef RadioOptionsGetter = List Function(); +typedef RadioCurOptionGetter = Future Function(); +typedef RadioOptionSetter = Future Function( + String oldValue, String newValue); + +class MenuEntryRadioUtils {} + +class MenuEntryRadios extends MenuEntryBase { + final String text; + final RadioOptionsGetter optionsGetter; + final RadioCurOptionGetter curOptionGetter; + final RadioOptionSetter optionSetter; + final RxString _curOption = "".obs; + + MenuEntryRadios( + {required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked) { + () async { + _curOption.value = await curOptionGetter(); + }(); + } + + List get options => optionsGetter(); + RxString get curOption => _curOption; + setOption(String option) async { + await optionSetter(_curOption.value, option); + if (_curOption.value != option) { + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } + } + + mod_menu.PopupMenuEntry _buildMenuItem( + BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { + return mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: Row( + children: [ + Text( + opt.text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 20.0, + height: 20.0, + child: Obx(() => opt.value == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : const SizedBox.shrink())), + )), + ], + ), + ), + onPressed: () { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(opt.value); + }, + ), + ); + } + + @override + List> build( + BuildContext context, MenuConfig conf) { + return options.map((opt) => _buildMenuItem(context, conf, opt)).toList(); + } +} + +class MenuEntrySubRadios extends MenuEntryBase { + final String text; + final RadioOptionsGetter optionsGetter; + final RadioCurOptionGetter curOptionGetter; + final RadioOptionSetter optionSetter; + final RxString _curOption = "".obs; + + MenuEntrySubRadios( + {required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked) { + () async { + _curOption.value = await curOptionGetter(); + }(); + } + + List get options => optionsGetter(); + RxString get curOption => _curOption; + setOption(String option) async { + await optionSetter(_curOption.value, option); + if (_curOption.value != option) { + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } + } + + mod_menu.PopupMenuEntry _buildSecondMenu( + BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { + return mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: Row( + children: [ + Text( + opt.text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 20.0, + height: 20.0, + child: Obx(() => opt.value == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : const SizedBox.shrink())), + )), + ], + ), + ), + onPressed: () { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(opt.value); + }, + ), + ); + } + + @override + List> build( + BuildContext context, MenuConfig conf) { + return [ + PopupMenuChildrenItem( + padding: EdgeInsets.zero, + height: conf.height, + itemBuilder: (BuildContext context) => + options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), + child: Row(children: [ + const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.keyboard_arrow_right, + color: conf.commonColor, + ), + )) + ]), + ) + ]; + } +} + +typedef SwitchGetter = Future Function(); +typedef SwitchSetter = Future Function(bool); + +abstract class MenuEntrySwitchBase extends MenuEntryBase { + final String text; + + MenuEntrySwitchBase({required this.text, required dismissOnClicked}) + : super(dismissOnClicked: dismissOnClicked); + + RxBool get curOption; + Future setOption(bool option); + + @override + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + height: conf.height, + child: Row(children: [ + // const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Obx(() => Switch( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + )), + )) + ])), + onPressed: () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(!curOption.value); + }, + ), + ) + ]; + } +} + +class MenuEntrySwitch extends MenuEntrySwitchBase { + final SwitchGetter getter; + final SwitchSetter setter; + final RxBool _curOption = false.obs; + + MenuEntrySwitch( + {required String text, + required this.getter, + required this.setter, + dismissOnClicked = false}) + : super(text: text, dismissOnClicked: dismissOnClicked) { + () async { + _curOption.value = await getter(); + }(); + } + + @override + RxBool get curOption => _curOption; + @override + setOption(bool option) async { + await setter(option); + final opt = await getter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } +} + +typedef Switch2Getter = RxBool Function(); +typedef Switch2Setter = Future Function(bool); + +class MenuEntrySwitch2 extends MenuEntrySwitchBase { + final Switch2Getter getter; + final SwitchSetter setter; + + MenuEntrySwitch2( + {required String text, + required this.getter, + required this.setter, + dismissOnClicked = false}) + : super(text: text, dismissOnClicked: dismissOnClicked); + + @override + RxBool get curOption => getter(); + @override + setOption(bool option) async { + await setter(option); + } +} + +class MenuEntrySubMenu extends MenuEntryBase { + final String text; + final List> entries; + + MenuEntrySubMenu({required this.text, required this.entries}); + + @override + List> build( + BuildContext context, MenuConfig conf) { + return [ + PopupMenuChildrenItem( + height: conf.height, + padding: EdgeInsets.zero, + position: mod_menu.PopupMenuPosition.overSide, + itemBuilder: (BuildContext context) => entries + .map((entry) => entry.build(context, conf)) + .expand((i) => i) + .toList(), + child: Row(children: [ + const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.keyboard_arrow_right, + color: conf.commonColor, + ), + )) + ]), + ) + ]; + } +} + +class MenuEntryButton extends MenuEntryBase { + final Widget Function(TextStyle? style) childBuilder; + Function() proc; + + MenuEntryButton( + {required this.childBuilder, + required this.proc, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked); + + @override + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: childBuilder( + const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + onPressed: () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + proc(); + }, + ), + ) + ]; + } +} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart new file mode 100644 index 000000000..47536011d --- /dev/null +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -0,0 +1,619 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:get/get.dart'; +import 'package:rxdart/rxdart.dart' as rxdart; + +import '../../common.dart'; +import '../../mobile/widgets/dialog.dart'; +import '../../mobile/widgets/overlay.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import './popup_menu.dart'; +import './material_mod_popup_menu.dart' as mod_menu; + +class _MenubarTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 25.0; + static const double dividerHeight = 12.0; +} + +class RemoteMenubar extends StatefulWidget { + final String id; + final FFI ffi; + + const RemoteMenubar({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State createState() => _RemoteMenubarState(); +} + +class _RemoteMenubarState extends State { + final RxBool _show = false.obs; + final Rx _hideColor = Colors.white12.obs; + + bool get isFullscreen => Get.find(tag: 'fullscreen').isTrue; + void setFullscreen(bool v) { + Get.find(tag: 'fullscreen').value = v; + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topCenter, + child: Obx( + () => _show.value ? _buildMenubar(context) : _buildShowHide(context)), + ); + } + + Widget _buildShowHide(BuildContext context) { + return Obx(() => Tooltip( + message: translate(_show.value ? "Hide Menubar" : "Show Menubar"), + child: SizedBox( + width: 100, + height: 5, + child: TextButton( + onHover: (bool v) { + _hideColor.value = v ? Colors.white60 : Colors.white24; + }, + onPressed: () { + _show.value = !_show.value; + }, + child: Obx(() => Container( + color: _hideColor.value, + )))), + )); + } + + Widget _buildMenubar(BuildContext context) { + final List menubarItems = []; + if (!isWebDesktop) { + menubarItems.add(_buildFullscreen(context)); + if (widget.ffi.ffiModel.isPeerAndroid) { + menubarItems.add(IconButton( + tooltip: translate('Mobile Actions'), + color: _MenubarTheme.commonColor, + icon: const Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + )); + } + } + menubarItems.add(_buildMonitor(context)); + menubarItems.add(_buildControl(context)); + menubarItems.add(_buildDisplay(context)); + if (!isWeb) { + menubarItems.add(_buildChat(context)); + } + menubarItems.add(_buildClose(context)); + return PopupMenuTheme( + data: const PopupMenuThemeData( + textStyle: TextStyle(color: _MenubarTheme.commonColor)), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container( + color: Colors.white, + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + )), + _buildShowHide(context), + ])); + } + + Widget _buildFullscreen(BuildContext context) { + return IconButton( + tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), + onPressed: () { + setFullscreen(!isFullscreen); + }, + icon: Obx(() => isFullscreen + ? const Icon( + Icons.fullscreen_exit, + color: _MenubarTheme.commonColor, + ) + : const Icon( + Icons.fullscreen, + color: _MenubarTheme.commonColor, + )), + ); + } + + Widget _buildChat(BuildContext context) { + return IconButton( + tooltip: translate('Chat'), + onPressed: () { + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(); + }, + icon: const Icon( + Icons.message, + color: _MenubarTheme.commonColor, + ), + ); + } + + Widget _buildMonitor(BuildContext context) { + final pi = widget.ffi.ffiModel.pi; + return mod_menu.PopupMenuButton( + tooltip: translate('Select Monitor'), + padding: EdgeInsets.zero, + position: mod_menu.PopupMenuPosition.under, + icon: Stack( + alignment: Alignment.center, + children: [ + const Icon( + Icons.personal_video, + color: _MenubarTheme.commonColor, + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.9), + child: Obx(() { + RxInt display = CurrentDisplayState.find(widget.id); + return Text( + "${display.value + 1}/${pi.displays.length}", + style: const TextStyle( + color: _MenubarTheme.commonColor, fontSize: 8), + ); + }), + ) + ], + ), + itemBuilder: (BuildContext context) { + final List rowChildren = []; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add( + Stack( + alignment: Alignment.center, + children: [ + const Icon( + Icons.personal_video, + color: _MenubarTheme.commonColor, + ), + TextButton( + child: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: + const TextStyle(color: _MenubarTheme.commonColor), + ), + )), + onPressed: () { + RxInt display = CurrentDisplayState.find(widget.id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: widget.id, value: i); + pi.currentDisplay = i; + display.value = i; + } + }, + ) + ], + ), + ); + } + return >[ + mod_menu.PopupMenuItem( + height: _MenubarTheme.height, + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren), + ) + ]; + }, + ); + } + + Widget _buildControl(BuildContext context) { + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.bolt, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Control Actions'), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => _getControlMenu() + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + Widget _buildDisplay(BuildContext context) { + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.tv, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Display Settings'), + position: mod_menu.PopupMenuPosition.under, + onSelected: (String item) {}, + itemBuilder: (BuildContext context) => _getDisplayMenu() + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + Widget _buildClose(BuildContext context) { + return IconButton( + tooltip: translate('Close'), + onPressed: () { + clientClose(widget.ffi.dialogManager); + }, + icon: const Icon( + Icons.close, + color: _MenubarTheme.commonColor, + ), + ); + } + + List> _getControlMenu() { + final pi = widget.ffi.ffiModel.pi; + final perms = widget.ffi.ffiModel.permissions; + + final List> displayMenu = []; + + if (pi.version.isNotEmpty) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Refresh'), + style: style, + ), + proc: () { + bind.sessionRefresh(id: widget.id); + }, + dismissOnClicked: true, + )); + } + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('OS Password'), + style: style, + ), + proc: () { + showSetOSPassword(widget.id, false, widget.ffi.dialogManager); + }, + dismissOnClicked: true, + )); + + if (!isWebDesktop) { + if (perms['keyboard'] != false && perms['clipboard'] != false) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Paste'), + style: style, + ), + proc: () { + () async { + ClipboardData? data = + await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + bind.sessionInputString(id: widget.id, value: data.text ?? ""); + } + }(); + }, + dismissOnClicked: true, + )); + } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Reset canvas'), + style: style, + ), + proc: () { + widget.ffi.cursorModel.reset(); + }, + dismissOnClicked: true, + )); + } + + if (perms['keyboard'] != false) { + if (pi.platform == 'Linux' || pi.sasEnabled) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '${translate("Insert")} Ctrl + Alt + Del', + style: style, + ), + proc: () { + bind.sessionCtrlAltDel(id: widget.id); + }, + dismissOnClicked: true, + )); + } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert Lock'), + style: style, + ), + proc: () { + bind.sessionLockScreen(id: widget.id); + }, + dismissOnClicked: true, + )); + + if (pi.platform == 'Windows') { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + '${BlockInputState.find(widget.id).value ? "Unb" : "B"}lock user input'), + style: style, + )), + proc: () { + RxBool blockInput = BlockInputState.find(widget.id); + bind.sessionToggleOption( + id: widget.id, + value: '${blockInput.value ? "un" : ""}block-input'); + blockInput.value = !blockInput.value; + }, + dismissOnClicked: true, + )); + } + } + + if (gFFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Restart Remote Device'), + style: style, + ), + proc: () { + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); + }, + dismissOnClicked: true, + )); + } + + return displayMenu; + } + + List> _getDisplayMenu() { + final displayMenu = [ + MenuEntryRadios( + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), value: 'original'), + MenuEntryRadioOption( + text: translate('Scale adaptive'), value: 'adaptive'), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'view-style') ?? + 'adaptive'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "view-style", value: newValue); + widget.ffi.canvasModel.updateViewStyle(); + }), + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Scroll Style'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('ScrollAuto'), value: 'scrollauto'), + MenuEntryRadioOption( + text: translate('Scrollbar'), value: 'scrollbar'), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'scroll-style') ?? + ''; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "scroll-style", value: newValue); + widget.ffi.canvasModel.updateScrollStyle(); + }), + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Image Quality'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Good image quality'), value: 'best'), + MenuEntryRadioOption( + text: translate('Balanced'), value: 'balanced'), + MenuEntryRadioOption( + text: translate('Optimize reaction time'), value: 'low'), + MenuEntryRadioOption( + text: translate('Custom'), + value: 'custom', + dismissOnClicked: true), + ], + curOptionGetter: () async { + String quality = + await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; + if (quality == '') quality = 'balanced'; + return quality; + }, + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetImageQuality(id: widget.id, value: newValue); + } + + if (newValue == 'custom') { + final btnCancel = msgBoxButton(translate('Close'), () { + widget.ffi.dialogManager.dismissAll(); + }); + final quality = + await bind.sessionGetCustomImageQuality(id: widget.id); + final double initValue = quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : 50.0; + final RxDouble sliderValue = RxDouble(initValue); + final rxReplay = rxdart.ReplaySubject(); + rxReplay + .throttleTime(const Duration(milliseconds: 1000), + trailing: true, leading: false) + .listen((double v) { + () async { + await bind.sessionSetCustomImageQuality( + id: widget.id, value: v.toInt()); + }(); + }); + final slider = Obx(() { + return Slider( + value: sliderValue.value, + max: 100, + divisions: 100, + label: sliderValue.value.round().toString(), + onChanged: (double value) { + sliderValue.value = value; + rxReplay.add(value); + }, + ); + }); + msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', + slider, [btnCancel]); + } + }), + MenuEntryDivider(), + MenuEntrySwitch( + text: translate('Show remote cursor'), + getter: () async { + return bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }), + MenuEntrySwitch( + text: translate('Show quality monitor'), + getter: () async { + return bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-quality-monitor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-quality-monitor'); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }), + ]; + + final perms = widget.ffi.ffiModel.permissions; + final pi = widget.ffi.ffiModel.pi; + + if (perms['audio'] != false) { + displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio')); + } + if (perms['keyboard'] != false) { + if (perms['clipboard'] != false) { + displayMenu.add( + _createSwitchMenuEntry('Disable clipboard', 'disable-clipboard')); + } + displayMenu.add(_createSwitchMenuEntry( + 'Lock after session end', 'lock-after-session-end')); + if (pi.platform == 'Windows') { + displayMenu.add(MenuEntrySwitch2( + text: translate('Privacy mode'), + getter: () { + return PrivacyModeState.find(widget.id); + }, + setter: (bool v) async { + Navigator.pop(context); + await bind.sessionToggleOption( + id: widget.id, value: 'privacy-mode'); + })); + } + } + return displayMenu; + } + + MenuEntrySwitch _createSwitchMenuEntry(String text, String option) { + return MenuEntrySwitch( + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: widget.id, value: option); + }); + } +} + +void showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + 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) { + return CustomAlertDialog( + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); + if (text != "" && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + }, + child: Text(translate('OK')), + ), + ]); + }); +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 09f1ee4b5..38e724bad 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -5,9 +7,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:scroll_pos/scroll_pos.dart'; +import 'package:window_manager/window_manager.dart'; import '../../utils/multi_window_manager.dart'; @@ -33,6 +36,15 @@ class TabInfo { required this.page}); } +enum DesktopTabType { + main, + cm, + remoteScreen, + fileTransfer, + portForward, + rdp, +} + class DesktopTabState { final List tabs = []; final ScrollPosController scrollController = @@ -63,6 +75,7 @@ class DesktopTabController { state.update((val) { val!.tabs.add(tab); }); + state.value.scrollController.itemCount = state.value.tabs.length; toIndex = state.value.tabs.length - 1; assert(toIndex >= 0); } @@ -95,8 +108,16 @@ class DesktopTabController { void jumpTo(int index) { state.update((val) { val!.selected = index; - val.pageController.jumpToPage(index); - val.scrollController.scrollToItem(index, center: true, animate: true); + Future.delayed(Duration.zero, (() { + if (val.pageController.hasClients) { + val.pageController.jumpToPage(index); + } + if (val.scrollController.hasClients && + val.scrollController.canScroll && + val.scrollController.itemCount >= index) { + val.scrollController.scrollToItem(index, center: true, animate: true); + } + })); }); onSelected?.call(index); } @@ -113,11 +134,27 @@ class DesktopTabController { remove(state.value.selected); } } + + void clear() { + state.value.tabs.clear(); + state.refresh(); + } } +class TabThemeConf { + double iconSize; + TarBarTheme theme; + TabThemeConf({required this.iconSize, required this.theme}); +} + +typedef TabBuilder = Widget Function( + String key, Widget icon, Widget label, TabThemeConf themeConf); +typedef LabelGetter = Rx Function(String key); + class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; + final DesktopTabType tabType; final bool isMainWindow; final bool showTabBar; final bool showLogo; @@ -127,23 +164,31 @@ class DesktopTab extends StatelessWidget { final bool showClose; final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; + final VoidCallback? onClose; + final TabBuilder? tabBuilder; + final LabelGetter? labelGetter; final DesktopTabController controller; - late final state = controller.state; + Rx get state => controller.state; - DesktopTab( - {required this.controller, - required this.isMainWindow, - this.theme = const TarBarTheme.light(), - this.onTabClose, - this.showTabBar = true, - this.showLogo = true, - this.showTitle = true, - this.showMinimize = true, - this.showMaximize = true, - this.showClose = true, - this.pageViewBuilder, - this.tail}); + const DesktopTab({ + required this.controller, + required this.tabType, + this.theme = const TarBarTheme.light(), + this.onTabClose, + this.showTabBar = true, + this.showLogo = true, + this.showTitle = true, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true, + this.pageViewBuilder, + this.tail, + this.onClose, + this.tabBuilder, + this.labelGetter, + }) : isMainWindow = + tabType == DesktopTabType.main || tabType == DesktopTabType.cm; @override Widget build(BuildContext context) { @@ -172,11 +217,48 @@ class DesktopTab extends StatelessWidget { ]); } + Widget _buildBlock({required Widget child}) { + if (tabType != DesktopTabType.main) { + return child; + } + var block = false.obs; + return Obx(() => MouseRegion( + onEnter: (_) async { + if (!option2bool( + 'allow-remote-config-modification', + await bind.mainGetOption( + key: 'allow-remote-config-modification'))) { + var time0 = DateTime.now().millisecondsSinceEpoch; + await bind.mainCheckMouseTime(); + Timer(const Duration(milliseconds: 120), () async { + var d = time0 - await bind.mainGetMouseTime(); + if (d < 120) { + block.value = true; + } + }); + } + }, + onExit: (_) => block.value = false, + child: Stack( + children: [ + child, + Offstage( + offstage: !block.value, + child: Container( + color: Colors.black.withOpacity(0.5), + )), + ], + ), + )); + } + Widget _buildPageView() { - return Obx(() => PageView( - controller: state.value.pageController, - children: - state.value.tabs.map((tab) => tab.page).toList(growable: false))); + return _buildBlock( + child: Obx(() => PageView( + controller: state.value.pageController, + children: state.value.tabs + .map((tab) => tab.page) + .toList(growable: false)))); } Widget _buildBar() { @@ -185,6 +267,11 @@ class DesktopTab extends StatelessWidget { Expanded( child: Row( children: [ + Offstage( + offstage: !Platform.isMacOS, + child: const SizedBox( + width: 78, + )), Row(children: [ Offstage( offstage: !showLogo, @@ -217,6 +304,8 @@ class DesktopTab extends StatelessWidget { controller: controller, onTabClose: onTabClose, theme: theme, + tabBuilder: tabBuilder, + labelGetter: labelGetter, )), ), ], @@ -229,6 +318,7 @@ class DesktopTab extends StatelessWidget { showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, + onClose: onClose, ) ], ); @@ -242,6 +332,7 @@ class WindowActionPanel extends StatelessWidget { final bool showMinimize; final bool showMaximize; final bool showClose; + final VoidCallback? onClose; const WindowActionPanel( {Key? key, @@ -249,7 +340,8 @@ class WindowActionPanel extends StatelessWidget { required this.theme, this.showMinimize = true, this.showMaximize = true, - this.showClose = true}) + this.showClose = true, + this.onClose}) : super(key: key); @override @@ -323,8 +415,12 @@ class WindowActionPanel extends StatelessWidget { if (mainTab) { windowManager.close(); } else { - WindowController.fromWindowId(windowId!).close(); + // only hide for multi window, not close + Future.delayed(Duration.zero, () { + WindowController.fromWindowId(windowId!).hide(); + }); } + onClose?.call(); }, is_close: true, )), @@ -336,13 +432,20 @@ class WindowActionPanel extends StatelessWidget { // ignore: must_be_immutable class _ListView extends StatelessWidget { final DesktopTabController controller; - late final Rx state; final Function(String key)? onTabClose; final TarBarTheme theme; + final TabBuilder? tabBuilder; + final LabelGetter? labelGetter; + + Rx get state => controller.state; + _ListView( - {required this.controller, required this.onTabClose, required this.theme}) - : this.state = controller.state; + {required this.controller, + required this.onTabClose, + required this.theme, + this.tabBuilder, + this.labelGetter}); @override Widget build(BuildContext context) { @@ -356,7 +459,9 @@ class _ListView extends StatelessWidget { final tab = e.value; return _Tab( index: index, - label: tab.label, + label: labelGetter == null + ? Rx(tab.label) + : labelGetter!(tab.label), selectedIcon: tab.selectedIcon, unselectedIcon: tab.unselectedIcon, closable: tab.closable, @@ -364,22 +469,33 @@ class _ListView extends StatelessWidget { onClose: () => controller.remove(index), onSelected: () => controller.jumpTo(index), theme: theme, + tabBuilder: tabBuilder == null + ? null + : (Widget icon, Widget labelWidget, TabThemeConf themeConf) { + return tabBuilder!( + tab.label, + icon, + labelWidget, + themeConf, + ); + }, ); }).toList())); } } -class _Tab extends StatelessWidget { +class _Tab extends StatefulWidget { late final int index; - late final String label; + late final Rx label; late final IconData? selectedIcon; late final IconData? unselectedIcon; late final bool closable; late final int selected; late final Function() onClose; late final Function() onSelected; - final RxBool _hover = false.obs; late final TarBarTheme theme; + final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? + tabBuilder; _Tab( {Key? key, @@ -387,6 +503,7 @@ class _Tab extends StatelessWidget { required this.label, this.selectedIcon, this.unselectedIcon, + this.tabBuilder, required this.closable, required this.selected, required this.onClose, @@ -394,61 +511,87 @@ class _Tab extends StatelessWidget { required this.theme}) : super(key: key); + @override + State<_Tab> createState() => _TabState(); +} + +class _TabState extends State<_Tab> with RestorationMixin { + final RestorableBool restoreHover = RestorableBool(false); + + Widget _buildTabContent() { + bool showIcon = + widget.selectedIcon != null && widget.unselectedIcon != null; + bool isSelected = widget.index == widget.selected; + + final icon = Offstage( + offstage: !showIcon, + child: Icon( + isSelected ? widget.selectedIcon : widget.unselectedIcon, + size: _kIconSize, + color: isSelected + ? widget.theme.selectedtabIconColor + : widget.theme.unSelectedtabIconColor, + ).paddingOnly(right: 5)); + final labelWidget = Obx(() { + return Text( + translate(widget.label.value), + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? widget.theme.selectedTextColor + : widget.theme.unSelectedTextColor), + ); + }); + + if (widget.tabBuilder == null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + labelWidget, + ], + ); + } else { + return widget.tabBuilder!(icon, labelWidget, + TabThemeConf(iconSize: _kIconSize, theme: widget.theme)); + } + } + @override Widget build(BuildContext context) { - bool show_icon = selectedIcon != null && unselectedIcon != null; - bool is_selected = index == selected; - bool show_divider = index != selected - 1 && index != selected; + bool isSelected = widget.index == widget.selected; + bool showDivider = + widget.index != widget.selected - 1 && widget.index != widget.selected; + RxBool hover = restoreHover.value.obs; return Ink( child: InkWell( - onHover: (hover) => _hover.value = hover, - onTap: () => onSelected(), + onHover: (value) { + hover.value = value; + restoreHover.value = value; + }, + onTap: () => widget.onSelected(), child: Row( children: [ - Container( + SizedBox( height: _kTabBarHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Offstage( - offstage: !show_icon, - child: Icon( - is_selected ? selectedIcon : unselectedIcon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5)), - Text( - translate(label), - textAlign: TextAlign.center, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ], - ), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ))), - ) + _buildTabContent(), + Obx((() => _CloseButton( + visiable: hover.value && widget.closable, + tabSelected: isSelected, + onClose: () => widget.onClose(), + theme: widget.theme, + ))) ])).paddingSymmetric(horizontal: 10), Offstage( - offstage: !show_divider, + offstage: !showDivider, child: VerticalDivider( width: 1, indent: _kDividerIndent, endIndent: _kDividerIndent, - color: theme.dividerColor, + color: widget.theme.dividerColor, thickness: 1, ), ) @@ -457,6 +600,14 @@ class _Tab extends StatelessWidget { ), ); } + + @override + String? get restorationId => "_Tab${widget.label.value}"; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(restoreHover, 'restoreHover'); + } } class _CloseButton extends StatelessWidget { @@ -480,7 +631,7 @@ class _CloseButton extends StatelessWidget { child: Offstage( offstage: !visiable, child: InkWell( - customBorder: RoundedRectangleBorder(), + customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), child: Icon( Icons.close, diff --git a/flutter/lib/generated_bridge.dart b/flutter/lib/generated_bridge.dart index b4df7c605..2cbeb430e 100644 --- a/flutter/lib/generated_bridge.dart +++ b/flutter/lib/generated_bridge.dart @@ -27,8 +27,13 @@ abstract class Rustdesk { Future hostStopSystemKeyPropagate( {required bool stopped, dynamic hint}); - Stream sessionConnect( - {required String id, required bool isFileTransfer, dynamic hint}); + String sessionAddSync( + {required String id, + required bool isFileTransfer, + required bool isPortForward, + dynamic hint}); + + Stream sessionStart({required String id, dynamic hint}); Future sessionGetRemember({required String id, dynamic hint}); @@ -38,8 +43,6 @@ abstract class Rustdesk { bool sessionGetToggleOptionSync( {required String id, required String arg, dynamic hint}); - Future sessionGetImageQuality({required String id, dynamic hint}); - Future sessionGetOption( {required String id, required String arg, dynamic hint}); @@ -58,9 +61,17 @@ abstract class Rustdesk { Future sessionToggleOption( {required String id, required String value, dynamic hint}); + Future sessionGetImageQuality({required String id, dynamic hint}); + Future sessionSetImageQuality( {required String id, required String value, dynamic hint}); + Future sessionGetCustomImageQuality( + {required String id, dynamic hint}); + + Future sessionSetCustomImageQuality( + {required String id, required int value, dynamic hint}); + Future sessionLockScreen({required String id, dynamic hint}); Future sessionCtrlAltDel({required String id, dynamic hint}); @@ -68,13 +79,6 @@ abstract class Rustdesk { Future sessionSwitchDisplay( {required String id, required int value, dynamic hint}); - Future sessionInputRawKey( - {required String id, - required int keycode, - required int scancode, - required bool down, - dynamic hint}); - Future sessionInputKey( {required String id, required String name, @@ -274,6 +278,16 @@ abstract class Rustdesk { Future mainLoadLanPeers({dynamic hint}); + Future sessionAddPortForward( + {required String id, + required int localPort, + required String remoteHost, + required int remotePort, + dynamic hint}); + + Future sessionRemovePortForward( + {required String id, required int localPort, dynamic hint}); + Future mainGetLastRemoteId({dynamic hint}); Future mainGetSoftwareUpdateUrl({dynamic hint}); @@ -320,6 +334,10 @@ abstract class Rustdesk { Future mainCheckSuperUserPermission({dynamic hint}); + Future mainCheckMouseTime({dynamic hint}); + + Future mainGetMouseTime({dynamic hint}); + Future cmSendChat( {required int connId, required String msg, dynamic hint}); @@ -413,17 +431,32 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); - Stream sessionConnect( - {required String id, required bool isFileTransfer, dynamic hint}) => + String sessionAddSync( + {required String id, + required bool isFileTransfer, + required bool isPortForward, + dynamic hint}) => + executeSync(FlutterRustBridgeSyncTask( + callFfi: () => inner.wire_session_add_sync( + _api2wire_String(id), isFileTransfer, isPortForward), + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_add_sync", + argNames: ["id", "isFileTransfer", "isPortForward"], + ), + argValues: [id, isFileTransfer, isPortForward], + hint: hint, + )); + + Stream sessionStart({required String id, dynamic hint}) => executeStream(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_connect( - port_, _api2wire_String(id), isFileTransfer), + callFfi: (port_) => + inner.wire_session_start(port_, _api2wire_String(id)), parseSuccessData: _wire2api_event_to_ui, constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_connect", - argNames: ["id", "isFileTransfer"], + debugName: "session_start", + argNames: ["id"], ), - argValues: [id, isFileTransfer], + argValues: [id], hint: hint, )); @@ -467,19 +500,6 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); - Future sessionGetImageQuality({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_get_image_quality(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_opt_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_image_quality", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - Future sessionGetOption( {required String id, required String arg, dynamic hint}) => executeNormal(FlutterRustBridgeTask( @@ -564,6 +584,19 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); + Future sessionGetImageQuality({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_get_image_quality(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_opt_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_image_quality", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + Future sessionSetImageQuality( {required String id, required String value, dynamic hint}) => executeNormal(FlutterRustBridgeTask( @@ -578,6 +611,34 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); + Future sessionGetCustomImageQuality( + {required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_get_custom_image_quality( + port_, _api2wire_String(id)), + parseSuccessData: _wire2api_opt_int_32_list, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_custom_image_quality", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionSetCustomImageQuality( + {required String id, required int value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_set_custom_image_quality( + port_, _api2wire_String(id), _api2wire_i32(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_set_custom_image_quality", + argNames: ["id", "value"], + ), + argValues: [id, value], + hint: hint, + )); + Future sessionLockScreen({required String id, dynamic hint}) => executeNormal(FlutterRustBridgeTask( callFfi: (port_) => @@ -618,28 +679,6 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); - Future sessionInputRawKey( - {required String id, - required int keycode, - required int scancode, - required bool down, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_input_raw_key( - port_, - _api2wire_String(id), - _api2wire_i32(keycode), - _api2wire_i32(scancode), - down), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_input_raw_key", - argNames: ["id", "keycode", "scancode", "down"], - ), - argValues: [id, keycode, scancode, down], - hint: hint, - )); - Future sessionInputKey( {required String id, required String name, @@ -1504,6 +1543,42 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); + Future sessionAddPortForward( + {required String id, + required int localPort, + required String remoteHost, + required int remotePort, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_add_port_forward( + port_, + _api2wire_String(id), + _api2wire_i32(localPort), + _api2wire_String(remoteHost), + _api2wire_i32(remotePort)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_add_port_forward", + argNames: ["id", "localPort", "remoteHost", "remotePort"], + ), + argValues: [id, localPort, remoteHost, remotePort], + hint: hint, + )); + + Future sessionRemovePortForward( + {required String id, required int localPort, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_remove_port_forward( + port_, _api2wire_String(id), _api2wire_i32(localPort)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_remove_port_forward", + argNames: ["id", "localPort"], + ), + argValues: [id, localPort], + hint: hint, + )); + Future mainGetLastRemoteId({dynamic hint}) => executeNormal(FlutterRustBridgeTask( callFfi: (port_) => inner.wire_main_get_last_remote_id(port_), @@ -1779,6 +1854,30 @@ class RustdeskImpl extends FlutterRustBridgeBase hint: hint, )); + Future mainCheckMouseTime({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_check_mouse_time(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_check_mouse_time", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetMouseTime({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_mouse_time(port_), + parseSuccessData: _wire2api_f64, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_mouse_time", + argNames: [], + ), + argValues: [], + hint: hint, + )); + Future cmSendChat( {required int connId, required String msg, dynamic hint}) => executeNormal(FlutterRustBridgeTask( @@ -1935,6 +2034,10 @@ List _wire2api_StringList(dynamic raw) { return (raw as List).cast(); } +String _wire2api_SyncReturnString(dynamic raw) { + return raw as String; +} + Uint8List _wire2api_ZeroCopyBuffer_Uint8List(dynamic raw) { return raw as Uint8List; } @@ -1966,10 +2069,18 @@ double _wire2api_f64(dynamic raw) { return raw as double; } +int _wire2api_i32(dynamic raw) { + return raw as int; +} + int _wire2api_i64(dynamic raw) { return raw as int; } +Int32List _wire2api_int_32_list(dynamic raw) { + return raw as Int32List; +} + String? _wire2api_opt_String(dynamic raw) { return raw == null ? null : _wire2api_String(raw); } @@ -1978,6 +2089,10 @@ bool? _wire2api_opt_box_autoadd_bool(dynamic raw) { return raw == null ? null : _wire2api_box_autoadd_bool(raw); } +Int32List? _wire2api_opt_int_32_list(dynamic raw) { + return raw == null ? null : _wire2api_int_32_list(raw); +} + int _wire2api_u8(dynamic raw) { return raw as int; } @@ -2063,7 +2178,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { void wire_host_stop_system_key_propagate( int port_, - bool stopped, + ffi.Pointer stopped, ) { return _wire_host_stop_system_key_propagate( port_, @@ -2071,31 +2186,49 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ); } - late final _wire_host_stop_system_key_propagatePtr = - _lookup>( - 'wire_host_stop_system_key_propagate'); + late final _wire_host_stop_system_key_propagatePtr = _lookup< + ffi.NativeFunction)>>( + 'wire_host_stop_system_key_propagate'); late final _wire_host_stop_system_key_propagate = _wire_host_stop_system_key_propagatePtr - .asFunction(); + .asFunction)>(); - void wire_session_connect( - int port_, + WireSyncReturnStruct wire_session_add_sync( ffi.Pointer id, - bool is_file_transfer, + ffi.Pointer is_file_transfer, + ffi.Pointer is_port_forward, ) { - return _wire_session_connect( - port_, + return _wire_session_add_sync( id, is_file_transfer, + is_port_forward, ); } - late final _wire_session_connectPtr = _lookup< + late final _wire_session_add_syncPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Bool)>>('wire_session_connect'); - late final _wire_session_connect = _wire_session_connectPtr - .asFunction, bool)>(); + WireSyncReturnStruct Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>('wire_session_add_sync'); + late final _wire_session_add_sync = _wire_session_add_syncPtr.asFunction< + WireSyncReturnStruct Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + void wire_session_start( + int port_, + ffi.Pointer id, + ) { + return _wire_session_start( + port_, + id, + ); + } + + late final _wire_session_startPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_session_start'); + late final _wire_session_start = _wire_session_startPtr + .asFunction)>(); void wire_session_get_remember( int port_, @@ -2156,24 +2289,6 @@ class RustdeskWire implements FlutterRustBridgeWireBase { WireSyncReturnStruct Function( ffi.Pointer, ffi.Pointer)>(); - void wire_session_get_image_quality( - int port_, - ffi.Pointer id, - ) { - return _wire_session_get_image_quality( - port_, - id, - ); - } - - late final _wire_session_get_image_qualityPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_session_get_image_quality'); - late final _wire_session_get_image_quality = - _wire_session_get_image_qualityPtr - .asFunction)>(); - void wire_session_get_option( int port_, ffi.Pointer id, @@ -2198,7 +2313,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { int port_, ffi.Pointer id, ffi.Pointer password, - bool remember, + ffi.Pointer remember, ) { return _wire_session_login( port_, @@ -2210,11 +2325,14 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_session_loginPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer, ffi.Bool)>>('wire_session_login'); + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_session_login'); late final _wire_session_login = _wire_session_loginPtr.asFunction< void Function(int, ffi.Pointer, - ffi.Pointer, bool)>(); + ffi.Pointer, ffi.Pointer)>(); void wire_session_close( int port_, @@ -2288,6 +2406,24 @@ class RustdeskWire implements FlutterRustBridgeWireBase { void Function(int, ffi.Pointer, ffi.Pointer)>(); + void wire_session_get_image_quality( + int port_, + ffi.Pointer id, + ) { + return _wire_session_get_image_quality( + port_, + id, + ); + } + + late final _wire_session_get_image_qualityPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_session_get_image_quality'); + late final _wire_session_get_image_quality = + _wire_session_get_image_qualityPtr + .asFunction)>(); + void wire_session_set_image_quality( int port_, ffi.Pointer id, @@ -2310,6 +2446,44 @@ class RustdeskWire implements FlutterRustBridgeWireBase { void Function(int, ffi.Pointer, ffi.Pointer)>(); + void wire_session_get_custom_image_quality( + int port_, + ffi.Pointer id, + ) { + return _wire_session_get_custom_image_quality( + port_, + id, + ); + } + + late final _wire_session_get_custom_image_qualityPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_session_get_custom_image_quality'); + late final _wire_session_get_custom_image_quality = + _wire_session_get_custom_image_qualityPtr + .asFunction)>(); + + void wire_session_set_custom_image_quality( + int port_, + ffi.Pointer id, + int value, + ) { + return _wire_session_set_custom_image_quality( + port_, + id, + value, + ); + } + + late final _wire_session_set_custom_image_qualityPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Int32)>>('wire_session_set_custom_image_quality'); + late final _wire_session_set_custom_image_quality = + _wire_session_set_custom_image_qualityPtr + .asFunction, int)>(); + void wire_session_lock_screen( int port_, ffi.Pointer id, @@ -2363,40 +2537,16 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_session_switch_display = _wire_session_switch_displayPtr .asFunction, int)>(); - void wire_session_input_raw_key( - int port_, - ffi.Pointer id, - int keycode, - int scancode, - bool down, - ) { - return _wire_session_input_raw_key( - port_, - id, - keycode, - scancode, - down, - ); - } - - late final _wire_session_input_raw_keyPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, ffi.Int32, - ffi.Int32, ffi.Bool)>>('wire_session_input_raw_key'); - late final _wire_session_input_raw_key = - _wire_session_input_raw_keyPtr.asFunction< - void Function(int, ffi.Pointer, int, int, bool)>(); - void wire_session_input_key( int port_, ffi.Pointer id, ffi.Pointer name, - bool down, - bool press, - bool alt, - bool ctrl, - bool shift, - bool command, + ffi.Pointer down, + ffi.Pointer press, + ffi.Pointer alt, + ffi.Pointer ctrl, + ffi.Pointer shift, + ffi.Pointer command, ) { return _wire_session_input_key( port_, @@ -2417,15 +2567,23 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Int64, ffi.Pointer, ffi.Pointer, - ffi.Bool, - ffi.Bool, - ffi.Bool, - ffi.Bool, - ffi.Bool, - ffi.Bool)>>('wire_session_input_key'); + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_session_input_key'); late final _wire_session_input_key = _wire_session_input_keyPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, bool, bool, bool, bool, bool, bool)>(); + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>(); void wire_session_input_string( int port_, @@ -2541,7 +2699,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { int port_, ffi.Pointer id, ffi.Pointer path, - bool include_hidden, + ffi.Pointer include_hidden, ) { return _wire_session_read_remote_dir( port_, @@ -2557,11 +2715,11 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Int64, ffi.Pointer, ffi.Pointer, - ffi.Bool)>>('wire_session_read_remote_dir'); + ffi.Pointer)>>('wire_session_read_remote_dir'); late final _wire_session_read_remote_dir = _wire_session_read_remote_dirPtr.asFunction< void Function(int, ffi.Pointer, - ffi.Pointer, bool)>(); + ffi.Pointer, ffi.Pointer)>(); void wire_session_send_files( int port_, @@ -2570,8 +2728,8 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer path, ffi.Pointer to, int file_num, - bool include_hidden, - bool is_remote, + ffi.Pointer include_hidden, + ffi.Pointer is_remote, ) { return _wire_session_send_files( port_, @@ -2594,8 +2752,8 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Pointer, ffi.Int32, - ffi.Bool, - ffi.Bool)>>('wire_session_send_files'); + ffi.Pointer, + ffi.Pointer)>>('wire_session_send_files'); late final _wire_session_send_files = _wire_session_send_filesPtr.asFunction< void Function( int, @@ -2604,17 +2762,17 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Pointer, int, - bool, - bool)>(); + ffi.Pointer, + ffi.Pointer)>(); void wire_session_set_confirm_override_file( int port_, ffi.Pointer id, int act_id, int file_num, - bool need_override, - bool remember, - bool is_upload, + ffi.Pointer need_override, + ffi.Pointer remember, + ffi.Pointer is_upload, ) { return _wire_session_set_confirm_override_file( port_, @@ -2634,13 +2792,13 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Int32, ffi.Int32, - ffi.Bool, - ffi.Bool, - ffi.Bool)>>('wire_session_set_confirm_override_file'); + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_session_set_confirm_override_file'); late final _wire_session_set_confirm_override_file = _wire_session_set_confirm_override_filePtr.asFunction< - void Function(int, ffi.Pointer, int, int, bool, - bool, bool)>(); + void Function(int, ffi.Pointer, int, int, + ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); void wire_session_remove_file( int port_, @@ -2648,7 +2806,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { int act_id, ffi.Pointer path, int file_num, - bool is_remote, + ffi.Pointer is_remote, ) { return _wire_session_remove_file( port_, @@ -2668,19 +2826,19 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Int32, ffi.Pointer, ffi.Int32, - ffi.Bool)>>('wire_session_remove_file'); + ffi.Pointer)>>('wire_session_remove_file'); late final _wire_session_remove_file = _wire_session_remove_filePtr.asFunction< void Function(int, ffi.Pointer, int, - ffi.Pointer, int, bool)>(); + ffi.Pointer, int, ffi.Pointer)>(); void wire_session_read_dir_recursive( int port_, ffi.Pointer id, int act_id, ffi.Pointer path, - bool is_remote, - bool show_hidden, + ffi.Pointer is_remote, + ffi.Pointer show_hidden, ) { return _wire_session_read_dir_recursive( port_, @@ -2699,19 +2857,24 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Int32, ffi.Pointer, - ffi.Bool, - ffi.Bool)>>('wire_session_read_dir_recursive'); + ffi.Pointer, + ffi.Pointer)>>('wire_session_read_dir_recursive'); late final _wire_session_read_dir_recursive = _wire_session_read_dir_recursivePtr.asFunction< - void Function(int, ffi.Pointer, int, - ffi.Pointer, bool, bool)>(); + void Function( + int, + ffi.Pointer, + int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>(); void wire_session_remove_all_empty_dirs( int port_, ffi.Pointer id, int act_id, ffi.Pointer path, - bool is_remote, + ffi.Pointer is_remote, ) { return _wire_session_remove_all_empty_dirs( port_, @@ -2729,11 +2892,11 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Int32, ffi.Pointer, - ffi.Bool)>>('wire_session_remove_all_empty_dirs'); + ffi.Pointer)>>('wire_session_remove_all_empty_dirs'); late final _wire_session_remove_all_empty_dirs = _wire_session_remove_all_empty_dirsPtr.asFunction< void Function(int, ffi.Pointer, int, - ffi.Pointer, bool)>(); + ffi.Pointer, ffi.Pointer)>(); void wire_session_cancel_job( int port_, @@ -2759,7 +2922,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer id, int act_id, ffi.Pointer path, - bool is_remote, + ffi.Pointer is_remote, ) { return _wire_session_create_dir( port_, @@ -2777,16 +2940,16 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Int32, ffi.Pointer, - ffi.Bool)>>('wire_session_create_dir'); + ffi.Pointer)>>('wire_session_create_dir'); late final _wire_session_create_dir = _wire_session_create_dirPtr.asFunction< void Function(int, ffi.Pointer, int, - ffi.Pointer, bool)>(); + ffi.Pointer, ffi.Pointer)>(); void wire_session_read_local_dir_sync( int port_, ffi.Pointer id, ffi.Pointer path, - bool show_hidden, + ffi.Pointer show_hidden, ) { return _wire_session_read_local_dir_sync( port_, @@ -2802,16 +2965,16 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Int64, ffi.Pointer, ffi.Pointer, - ffi.Bool)>>('wire_session_read_local_dir_sync'); + ffi.Pointer)>>('wire_session_read_local_dir_sync'); late final _wire_session_read_local_dir_sync = _wire_session_read_local_dir_syncPtr.asFunction< void Function(int, ffi.Pointer, - ffi.Pointer, bool)>(); + ffi.Pointer, ffi.Pointer)>(); void wire_session_get_platform( int port_, ffi.Pointer id, - bool is_remote, + ffi.Pointer is_remote, ) { return _wire_session_get_platform( port_, @@ -2823,9 +2986,11 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_session_get_platformPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Bool)>>('wire_session_get_platform'); - late final _wire_session_get_platform = _wire_session_get_platformPtr - .asFunction, bool)>(); + ffi.Pointer)>>('wire_session_get_platform'); + late final _wire_session_get_platform = + _wire_session_get_platformPtr.asFunction< + void Function( + int, ffi.Pointer, ffi.Pointer)>(); void wire_session_load_last_transfer_jobs( int port_, @@ -2852,8 +3017,8 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer path, ffi.Pointer to, int file_num, - bool include_hidden, - bool is_remote, + ffi.Pointer include_hidden, + ffi.Pointer is_remote, ) { return _wire_session_add_job( port_, @@ -2876,8 +3041,8 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Pointer, ffi.Int32, - ffi.Bool, - ffi.Bool)>>('wire_session_add_job'); + ffi.Pointer, + ffi.Pointer)>>('wire_session_add_job'); late final _wire_session_add_job = _wire_session_add_jobPtr.asFunction< void Function( int, @@ -2886,14 +3051,14 @@ class RustdeskWire implements FlutterRustBridgeWireBase { ffi.Pointer, ffi.Pointer, int, - bool, - bool)>(); + ffi.Pointer, + ffi.Pointer)>(); void wire_session_resume_job( int port_, ffi.Pointer id, int act_id, - bool is_remote, + ffi.Pointer is_remote, ) { return _wire_session_resume_job( port_, @@ -2906,9 +3071,10 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_session_resume_jobPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Int64, ffi.Pointer, ffi.Int32, - ffi.Bool)>>('wire_session_resume_job'); + ffi.Pointer)>>('wire_session_resume_job'); late final _wire_session_resume_job = _wire_session_resume_jobPtr.asFunction< - void Function(int, ffi.Pointer, int, bool)>(); + void Function( + int, ffi.Pointer, int, ffi.Pointer)>(); void wire_main_get_sound_inputs( int port_, @@ -3479,6 +3645,55 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_main_load_lan_peers = _wire_main_load_lan_peersPtr.asFunction(); + void wire_session_add_port_forward( + int port_, + ffi.Pointer id, + int local_port, + ffi.Pointer remote_host, + int remote_port, + ) { + return _wire_session_add_port_forward( + port_, + id, + local_port, + remote_host, + remote_port, + ); + } + + late final _wire_session_add_port_forwardPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Int32)>>('wire_session_add_port_forward'); + late final _wire_session_add_port_forward = + _wire_session_add_port_forwardPtr.asFunction< + void Function(int, ffi.Pointer, int, + ffi.Pointer, int)>(); + + void wire_session_remove_port_forward( + int port_, + ffi.Pointer id, + int local_port, + ) { + return _wire_session_remove_port_forward( + port_, + id, + local_port, + ); + } + + late final _wire_session_remove_port_forwardPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Int32)>>('wire_session_remove_port_forward'); + late final _wire_session_remove_port_forward = + _wire_session_remove_port_forwardPtr + .asFunction, int)>(); + void wire_main_get_last_remote_id( int port_, ) { @@ -3819,6 +4034,34 @@ class RustdeskWire implements FlutterRustBridgeWireBase { _wire_main_check_super_user_permissionPtr .asFunction(); + void wire_main_check_mouse_time( + int port_, + ) { + return _wire_main_check_mouse_time( + port_, + ); + } + + late final _wire_main_check_mouse_timePtr = + _lookup>( + 'wire_main_check_mouse_time'); + late final _wire_main_check_mouse_time = + _wire_main_check_mouse_timePtr.asFunction(); + + void wire_main_get_mouse_time( + int port_, + ) { + return _wire_main_get_mouse_time( + port_, + ); + } + + late final _wire_main_get_mouse_timePtr = + _lookup>( + 'wire_main_get_mouse_time'); + late final _wire_main_get_mouse_time = + _wire_main_get_mouse_timePtr.asFunction(); + void wire_cm_send_chat( int port_, int conn_id, @@ -3841,7 +4084,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { void wire_cm_login_res( int port_, int conn_id, - bool res, + ffi.Pointer res, ) { return _wire_cm_login_res( port_, @@ -3853,9 +4096,9 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_cm_login_resPtr = _lookup< ffi.NativeFunction< ffi.Void Function( - ffi.Int64, ffi.Int32, ffi.Bool)>>('wire_cm_login_res'); - late final _wire_cm_login_res = - _wire_cm_login_resPtr.asFunction(); + ffi.Int64, ffi.Int32, ffi.Pointer)>>('wire_cm_login_res'); + late final _wire_cm_login_res = _wire_cm_login_resPtr + .asFunction)>(); void wire_cm_close_connection( int port_, @@ -3907,7 +4150,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { int port_, int conn_id, ffi.Pointer name, - bool enabled, + ffi.Pointer enabled, ) { return _wire_cm_switch_permission( port_, @@ -3920,10 +4163,11 @@ class RustdeskWire implements FlutterRustBridgeWireBase { late final _wire_cm_switch_permissionPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Int64, ffi.Int32, ffi.Pointer, - ffi.Bool)>>('wire_cm_switch_permission'); + ffi.Pointer)>>('wire_cm_switch_permission'); late final _wire_cm_switch_permission = _wire_cm_switch_permissionPtr.asFunction< - void Function(int, int, ffi.Pointer, bool)>(); + void Function( + int, int, ffi.Pointer, ffi.Pointer)>(); void wire_main_get_icon( int port_, @@ -4000,7 +4244,7 @@ class RustdeskWire implements FlutterRustBridgeWireBase { .asFunction(); void store_dart_post_cobject( - DartPostCObjectFnType ptr, + int ptr, ) { return _store_dart_post_cobject( ptr, @@ -4008,19 +4252,19 @@ class RustdeskWire implements FlutterRustBridgeWireBase { } late final _store_dart_post_cobjectPtr = - _lookup>( + _lookup>( 'store_dart_post_cobject'); - late final _store_dart_post_cobject = _store_dart_post_cobjectPtr - .asFunction(); + late final _store_dart_post_cobject = + _store_dart_post_cobjectPtr.asFunction(); - bool rustdesk_core_main() { + int rustdesk_core_main() { return _rustdesk_core_main(); } late final _rustdesk_core_mainPtr = - _lookup>('rustdesk_core_main'); + _lookup>('rustdesk_core_main'); late final _rustdesk_core_main = - _rustdesk_core_mainPtr.asFunction(); + _rustdesk_core_mainPtr.asFunction(); } class wire_uint_8_list extends ffi.Struct { @@ -4037,10 +4281,8 @@ class wire_StringList extends ffi.Struct { external int len; } +typedef bool = ffi.NativeFunction)>; typedef uintptr_t = ffi.UnsignedLong; -typedef DartPostCObjectFnType = ffi.Pointer< - ffi.NativeFunction)>>; -typedef DartPort = ffi.Int64; const int GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 9682f19d1..e1c254942 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -47,6 +48,9 @@ Future main(List args) async { case WindowType.FileTransfer: runFileTransferScreen(argument); break; + case WindowType.PortForward: + runPortForwardScreen(argument); + break; default: break; } @@ -76,14 +80,9 @@ Future initEnv(String appType) async { } void runMainApp(bool startService) async { - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720)); - await Future.wait([ - initEnv(kAppTypeMain), - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }) - ]); + await initEnv(kAppTypeMain); + // trigger connection status updater + await bind.mainCheckConnectStatus(); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -91,6 +90,13 @@ void runMainApp(bool startService) async { gFFI.serverModel.startService(); } runApp(App()); + // set window option + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(const Size(1280, 720)); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); } void runMobileApp() async { @@ -133,6 +139,23 @@ void runFileTransferScreen(Map argument) async { ); } +void runPortForwardScreen(Map argument) async { + await initEnv(kAppTypeDesktopPortForward); + runApp( + GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - Port Forward', + theme: getCurrentTheme(), + home: DesktopPortForwardScreen(params: argument), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + ], + builder: _keepScaleBuilder(), + ), + ); +} + void runConnectionManagerScreen() async { // initialize window WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); @@ -182,7 +205,7 @@ class App extends StatelessWidget { title: 'RustDesk', theme: getCurrentTheme(), home: isDesktop - ? DesktopTabPage() + ? const DesktopTabPage() : !isAndroid ? WebHomePage() : HomePage(), @@ -190,8 +213,13 @@ class App extends StatelessWidget { // FirebaseAnalyticsObserver(analytics: analytics), ], builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, + ? (context, child) => AccessibilityListener( + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1.0, + ), + child: child ?? Container(), + ), ) : _keepScaleBuilder(), ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 889e13d21..967a334e8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; @@ -17,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; import '../common.dart'; +import '../common/shared_state.dart'; import '../mobile/widgets/dialog.dart'; import '../mobile/widgets/overlay.dart'; import 'peer_model.dart'; @@ -96,25 +98,26 @@ class FfiModel with ChangeNotifier { clearPermissions(); } - void setConnectionType(bool secure, bool direct) { + void setConnectionType(String peerId, bool secure, bool direct) { _secure = secure; _direct = direct; + try { + var connectionType = ConnectionTypeState.find(peerId); + connectionType.setSecure(secure); + connectionType.setDirect(direct); + } catch (e) { + // + } } Image? getConnectionImage() { - String? icon; - if (secure == true && direct == true) { - icon = 'secure'; - } else if (secure == false && direct == true) { - icon = 'insecure'; - } else if (secure == false && direct == false) { - icon = 'insecure_relay'; - } else if (secure == true && direct == false) { - icon = 'secure_relay'; + if (secure == null || direct == null) { + return null; + } else { + final icon = + '${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}'; + return Image.asset('assets/$icon.png', width: 48, height: 48); } - return icon == null - ? null - : Image.asset('assets/$icon.png', width: 48, height: 48); } void clearPermissions() { @@ -130,7 +133,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { - setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true'); + setConnectionType( + peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { @@ -172,9 +176,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); } else if (name == 'update_block_input_state') { - updateBlockInputState(evt); + updateBlockInputState(evt, peerId); } else if (name == 'update_privacy_mode') { - updatePrivacyMode(evt); + updatePrivacyMode(evt, peerId); } }; } @@ -189,7 +193,7 @@ class FfiModel with ChangeNotifier { handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { parent.target?.ffiModel.setConnectionType( - evt['secure'] == 'true', evt['direct'] == 'true'); + peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { @@ -231,9 +235,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); } else if (name == 'update_block_input_state') { - updateBlockInputState(evt); + updateBlockInputState(evt, peerId); } else if (name == 'update_privacy_mode') { - updatePrivacyMode(evt); + updatePrivacyMode(evt, peerId); } }; platformFFI.setEventCallback(cb); @@ -297,6 +301,9 @@ class FfiModel with ChangeNotifier { /// Handle the peer info event based on [evt]. void handlePeerInfo(Map evt, String peerId) async { + // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) + bind.mainLoadRecentPeers(); + parent.target?.dialogManager.dismissAll(); _pi.version = evt['version']; _pi.username = evt['username']; @@ -305,6 +312,12 @@ class FfiModel with ChangeNotifier { _pi.sasEnabled = evt['sas_enabled'] == "true"; _pi.currentDisplay = int.parse(evt['current_display']); + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + if (isPeerAndroid) { _touchMode = true; if (parent.target?.ffiModel.permissions['keyboard'] != false) { @@ -316,6 +329,7 @@ class FfiModel with ChangeNotifier { } if (evt['is_file_transfer'] == "true") { + // TODO is file transfer parent.target?.fileModel.onReady(); } else { _pi.displays = []; @@ -343,13 +357,24 @@ class FfiModel with ChangeNotifier { notifyListeners(); } - updateBlockInputState(Map evt) { + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); + try { + BlockInputState.find(peerId).value = evt['input_state'] == 'on'; + } catch (e) { + // + } } - updatePrivacyMode(Map evt) { + updatePrivacyMode(Map evt, String peerId) { notifyListeners(); + try { + PrivacyModeState.find(peerId).value = + bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode'); + } catch (e) { + // + } } } @@ -476,39 +501,11 @@ class CanvasModel with ChangeNotifier { return; } - final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); - final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); - - // Closure to perform shrink operation. - final shrinkOp = () { - final s = s1 < s2 ? s1 : s2; - if (s < 1) { - _scale = s; - } - }; - // Closure to perform stretch operation. - final stretchOp = () { - final s = s1 < s2 ? s1 : s2; - if (s > 1) { - _scale = s; - } - }; - // Closure to perform default operation(set the scale to 1.0). - final defaultOp = () { - _scale = 1.0; - }; - - // // On desktop, shrink is the default behavior. - // if (isDesktop) { - // shrinkOp(); - // } else { - defaultOp(); - // } - - if (style == 'shrink') { - shrinkOp(); - } else if (style == 'stretch') { - stretchOp(); + _scale = 1.0; + if (style == 'adaptive') { + final s1 = size.width / getDisplayWidth(); + final s2 = size.height / getDisplayHeight(); + _scale = s1 < s2 ? s1 : s2; } _x = (size.width - getDisplayWidth() * _scale) / 2; @@ -536,11 +533,17 @@ class CanvasModel with ChangeNotifier { } int getDisplayWidth() { - return parent.target?.ffiModel.display.width ?? 1080; + final defaultWidth = (isDesktop || isWebDesktop) + ? kDesktopDefaultDisplayWidth + : kMobileDefaultDisplayWidth; + return parent.target?.ffiModel.display.width ?? defaultWidth; } int getDisplayHeight() { - return parent.target?.ffiModel.display.height ?? 720; + final defaultHeight = (isDesktop || isWebDesktop) + ? kDesktopDefaultDisplayHeight + : kMobileDefaultDisplayHeight; + return parent.target?.ffiModel.display.height ?? defaultHeight; } Size get size { @@ -556,9 +559,19 @@ class CanvasModel with ChangeNotifier { var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { + final X_debugNanOrInfinite = x - dw * (x / size.width) - _x; + if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) { + debugPrint( + 'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x'); + } dxOffset = (x - dw * (x / size.width) - _x).toInt(); } if (dh > size.height) { + final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y; + if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) { + debugPrint( + 'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y'); + } dyOffset = (y - dh * (y / size.height) - _y).toInt(); } _x += dxOffset; @@ -926,16 +939,16 @@ class FFI { late final QualityMonitorModel qualityMonitorModel; // session FFI() { - this.imageModel = ImageModel(WeakReference(this)); - this.ffiModel = FfiModel(WeakReference(this)); - this.cursorModel = CursorModel(WeakReference(this)); - this.canvasModel = CanvasModel(WeakReference(this)); - this.serverModel = ServerModel(WeakReference(this)); // use global FFI - this.chatModel = ChatModel(WeakReference(this)); - this.fileModel = FileModel(WeakReference(this)); - this.abModel = AbModel(WeakReference(this)); - this.userModel = UserModel(WeakReference(this)); - this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); + imageModel = ImageModel(WeakReference(this)); + ffiModel = FfiModel(WeakReference(this)); + cursorModel = CursorModel(WeakReference(this)); + canvasModel = CanvasModel(WeakReference(this)); + serverModel = ServerModel(WeakReference(this)); // use global FFI + chatModel = ChatModel(WeakReference(this)); + fileModel = FileModel(WeakReference(this)); + abModel = AbModel(WeakReference(this)); + userModel = UserModel(WeakReference(this)); + qualityMonitorModel = QualityMonitorModel(WeakReference(this)); } /// Send a mouse tap event(down and up). @@ -983,7 +996,7 @@ class FFI { // Raw Key void inputRawKey(int keyCode, int scanCode, bool down){ debugPrint(scanCode.toString()); - bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down); + // bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down); } /// Send key stroke event. @@ -1040,17 +1053,26 @@ class FFI { return []; } - /// Connect with the given [id]. Only transfer file if [isFileTransfer]. + /// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. void connect(String id, - {bool isFileTransfer = false, double tabBarHeight = 0.0}) { - if (!isFileTransfer) { + {bool isFileTransfer = false, + bool isPortForward = false, + double tabBarHeight = 0.0}) { + assert(!(isFileTransfer && isPortForward), "more than one connect type"); + if (isFileTransfer) { + id = 'ft_${id}'; + } else if (isPortForward) { + id = 'pf_${id}'; + } else { chatModel.resetClientMode(); canvasModel.id = id; imageModel._id = id; cursorModel.id = id; } - id = isFileTransfer ? 'ft_${id}' : id; - final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); + // ignore: unused_local_variable + final addRes = bind.sessionAddSync( + id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); + final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { @@ -1092,7 +1114,7 @@ class FFI { ffiModel.clear(); canvasModel.clear(); resetModifiers(); - print("model closed"); + debugPrint("model $id closed"); } /// Send **get** command to the Rust core based on [name] and [arg]. @@ -1236,20 +1258,20 @@ class PeerInfo { Future savePreference(String id, double xCursor, double yCursor, double xCanvas, double yCanvas, double scale, int currentDisplay) async { SharedPreferences prefs = await SharedPreferences.getInstance(); - final p = Map(); + final p = {}; p['xCursor'] = xCursor; p['yCursor'] = yCursor; p['xCanvas'] = xCanvas; p['yCanvas'] = yCanvas; p['scale'] = scale; p['currentDisplay'] = currentDisplay; - prefs.setString('peer' + id, json.encode(p)); + prefs.setString('peer$id', json.encode(p)); } Future?> getPreference(String id) async { if (!isWebDesktop) return null; SharedPreferences prefs = await SharedPreferences.getInstance(); - var p = prefs.getString('peer' + id); + var p = prefs.getString('peer$id'); if (p == null) return null; Map m = json.decode(p); return m; @@ -1257,7 +1279,7 @@ Future?> getPreference(String id) async { void removePreference(String id) async { SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('peer' + id); + prefs.remove('peer$id'); } void initializeCursorAndCanvas(FFI ffi) async { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 3359e1c4f..0d356a7c2 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,7 +30,7 @@ class PlatformFFI { String _dir = ''; String _homeDir = ''; F2? _translate; - var _eventHandlers = Map>(); + final _eventHandlers = Map>(); late RustdeskImpl _ffiBind; late String _appType; void Function(Map)? _eventCallback; @@ -50,27 +50,27 @@ class PlatformFFI { } bool registerEventHandler( - String event_name, String handler_name, HandleEvent handler) { - debugPrint('registerEventHandler $event_name $handler_name'); - var handlers = _eventHandlers[event_name]; + String eventName, String handlerName, HandleEvent handler) { + debugPrint('registerEventHandler $eventName $handlerName'); + var handlers = _eventHandlers[eventName]; if (handlers == null) { - _eventHandlers[event_name] = {handler_name: handler}; + _eventHandlers[eventName] = {handlerName: handler}; return true; } else { - if (handlers.containsKey(handler_name)) { + if (handlers.containsKey(handlerName)) { return false; } else { - handlers[handler_name] = handler; + handlers[handlerName] = handler; return true; } } } - void unregisterEventHandler(String event_name, String handler_name) { - debugPrint('unregisterEventHandler $event_name $handler_name'); - var handlers = _eventHandlers[event_name]; + void unregisterEventHandler(String eventName, String handlerName) { + debugPrint('unregisterEventHandler $eventName $handlerName'); + var handlers = _eventHandlers[eventName]; if (handlers != null) { - handlers.remove(handler_name); + handlers.remove(handlerName); } } @@ -117,7 +117,7 @@ class PlatformFFI { _homeDir = (await getDownloadsDirectory())?.path ?? ""; } } catch (e) { - print(e); + print("initialize failed: $e"); } String id = 'NA'; String name = 'Flutter'; @@ -151,7 +151,7 @@ class PlatformFFI { await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainInit(appDir: _dir); } catch (e) { - print(e); + print("initialize failed: $e"); } version = await getVersion(); } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 5c889e60f..79b71e6db 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -10,9 +10,8 @@ class Peer { final List tags; bool online = false; - Peer.fromJson(String id, Map json) - : id = id, - username = json['username'] ?? '', + Peer.fromJson(this.id, Map json) + : username = json['username'] ?? '', hostname = json['hostname'] ?? '', platform = json['platform'] ?? '', tags = json['tags'] ?? []; @@ -35,57 +34,52 @@ class Peer { } class Peers extends ChangeNotifier { - late String _name; - late List _peers; - late final _loadEvent; + final String name; + final String loadEvent; + List peers; static const _cbQueryOnlines = 'callback_query_onlines'; - Peers(String name, String loadEvent, List _initPeers) { - _name = name; - _loadEvent = loadEvent; - _peers = _initPeers; - platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) { + Peers({required this.name, required this.peers, required this.loadEvent}) { + platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) { _updateOnlineState(evt); }); - platformFFI.registerEventHandler(_loadEvent, _name, (evt) { + platformFFI.registerEventHandler(loadEvent, name, (evt) { _updatePeers(evt); }); } - List get peers => _peers; - @override void dispose() { - platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); - platformFFI.unregisterEventHandler(_loadEvent, _name); + platformFFI.unregisterEventHandler(_cbQueryOnlines, name); + platformFFI.unregisterEventHandler(loadEvent, name); super.dispose(); } Peer getByIndex(int index) { - if (index < _peers.length) { - return _peers[index]; + if (index < peers.length) { + return peers[index]; } else { return Peer.loading(); } } int getPeersCount() { - return _peers.length; + return peers.length; } void _updateOnlineState(Map evt) { evt['onlines'].split(',').forEach((online) { - for (var i = 0; i < _peers.length; i++) { - if (_peers[i].id == online) { - _peers[i].online = true; + for (var i = 0; i < peers.length; i++) { + if (peers[i].id == online) { + peers[i].online = true; } } }); evt['offlines'].split(',').forEach((offline) { - for (var i = 0; i < _peers.length; i++) { - if (_peers[i].id == offline) { - _peers[i].online = false; + for (var i = 0; i < peers.length; i++) { + if (peers[i].id == offline) { + peers[i].online = false; } } }); @@ -95,19 +89,19 @@ class Peers extends ChangeNotifier { void _updatePeers(Map evt) { final onlineStates = _getOnlineStates(); - _peers = _decodePeers(evt['peers']); - _peers.forEach((peer) { + peers = _decodePeers(evt['peers']); + for (var peer in peers) { final state = onlineStates[peer.id]; peer.online = state != null && state != false; - }); + } notifyListeners(); } Map _getOnlineStates() { - var onlineStates = new Map(); - _peers.forEach((peer) { + var onlineStates = {}; + for (var peer in peers) { onlineStates[peer.id] = peer.online; - }); + } return onlineStates; } @@ -121,7 +115,7 @@ class Peers extends ChangeNotifier { Peer.fromJson(s[0] as String, s[1] as Map)) .toList(); } catch (e) { - print('peers(): $e'); + debugPrint('peers(): $e'); } return []; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 9b26870c0..97d5a5e23 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -35,10 +34,11 @@ class RustDeskMultiWindowManager { int? _remoteDesktopWindowId; int? _fileTransferWindowId; + int? _portForwardWindowId; - Future new_remote_desktop(String remote_id) async { + Future newRemoteDesktop(String remoteId) async { final msg = - jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id}); + jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId}); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -62,9 +62,9 @@ class RustDeskMultiWindowManager { } } - Future new_file_transfer(String remote_id) async { + Future newFileTransfer(String remoteId) async { final msg = - jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id}); + jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId}); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -87,6 +87,31 @@ class RustDeskMultiWindowManager { } } + Future newPortForward(String remoteId, bool isRDP) async { + final msg = jsonEncode( + {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); + + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(_portForwardWindowId)) { + _portForwardWindowId = null; + } + } on Error { + _portForwardWindowId = null; + } + if (_portForwardWindowId == null) { + final portForwardController = await DesktopMultiWindow.createWindow(msg); + portForwardController + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle("rustdesk - port forward") + ..show(); + _portForwardWindowId = portForwardController.windowId; + } else { + return call(WindowType.PortForward, "new_port_forward", msg); + } + } + Future call(WindowType type, String methodName, dynamic args) async { int? windowId = findWindowByType(type); if (windowId == null) { @@ -104,7 +129,7 @@ class RustDeskMultiWindowManager { case WindowType.FileTransfer: return _fileTransferWindowId; case WindowType.PortForward: - break; + return _portForwardWindowId; case WindowType.Unknown: break; } @@ -120,7 +145,7 @@ class RustDeskMultiWindowManager { await Future.wait(WindowType.values.map((e) => closeWindows(e))); } - Future closeWindows(WindowType type) async { + Future closeWindows(WindowType type) async { if (type == WindowType.Main) { // skip main window, use window manager instead return; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 2a4558945..a6dade189 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -235,12 +235,10 @@ packages: dash_chat_2: dependency: "direct main" description: - path: "." - ref: feat_maxWidth - resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a" - url: "https://github.com/fufesou/Dash-Chat-2" - source: git - version: "0.0.12" + name: dash_chat_2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.14" desktop_drop: dependency: "direct main" description: @@ -252,8 +250,8 @@ packages: dependency: "direct main" description: path: "." - ref: e013c81d75320bbf28adddeaadf462264ee6039d - resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d + ref: e0368a023ba195462acc00d33ab361b499f0e413 + resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -849,7 +847,7 @@ packages: source: hosted version: "3.1.0" rxdart: - dependency: transitive + dependency: "direct main" description: name: rxdart url: "https://pub.flutter-io.cn" @@ -1244,8 +1242,8 @@ packages: dependency: "direct main" description: path: "." - ref: a25f1776ccc1119cbb2a8541174293aa36d532ed - resolved-ref: a25f1776ccc1119cbb2a8541174293aa36d532ed + ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.6" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e109be507..fecef77e6 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -34,16 +34,13 @@ dependencies: provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.5.2 - device_info_plus: ^4.0.2 + device_info_plus: ^4.1.2 firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 shared_preferences: ^2.0.6 toggle_switch: ^1.4.0 - dash_chat_2: - git: - url: https://github.com/fufesou/Dash-Chat-2 - ref: feat_maxWidth + dash_chat_2: ^0.0.14 draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 @@ -61,11 +58,11 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: a25f1776ccc1119cbb2a8541174293aa36d532ed + ref: 799ef079e87938c3f4340591b4330c2598f38bb9 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e013c81d75320bbf28adddeaadf462264ee6039d + ref: e0368a023ba195462acc00d33ab361b499f0e413 freezed_annotation: ^2.0.3 tray_manager: git: @@ -76,6 +73,7 @@ dependencies: contextmenu: ^3.0.0 desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 + rxdart: ^0.27.5 dev_dependencies: flutter_launcher_icons: ^0.9.1 diff --git a/flutter/web/js/gen_js_from_hbb.py b/flutter/web/js/gen_js_from_hbb.py index 13dbc96fc..0bdde54e4 100755 --- a/flutter/web/js/gen_js_from_hbb.py +++ b/flutter/web/js/gen_js_from_hbb.py @@ -5,25 +5,36 @@ import os import glob from tabnanny import check +def pad_start(s, n, c = ' '): + if len(s) >= n: + return s + return c * (n - len(s)) + s + +def safe_unicode(s): + res = "" + for c in s: + res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0')) + return res + def main(): print('export const LANGS = {') for fn in glob.glob('../../../src/lang/*'): lang = os.path.basename(fn)[:-3] if lang == 'template': continue print(' %s: {'%lang) - for ln in open(fn): + for ln in open(fn, encoding='utf-8'): ln = ln.strip() if ln.startswith('("'): toks = ln.split('", "') assert(len(toks) == 2) a = toks[0][2:] b = toks[1][:-3] - print(' "%s": "%s",'%(a, b)) + print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b))) print(' },') print('}') check_if_retry = ['', False] KEY_MAP = ['', False] - for ln in open('../../../src/client.rs'): + for ln in open('../../../src/client.rs', encoding='utf-8'): ln = ln.strip() if 'check_if_retry' in ln: check_if_retry[1] = True @@ -55,7 +66,7 @@ def main(): print('export const KEY_MAP: any = {') print(KEY_MAP[0]) print('}') - for ln in open('../../../Cargo.toml'): + for ln in open('../../../Cargo.toml', encoding='utf-8'): if ln.startswith('version ='): print('export const ' + ln) diff --git a/src/client.rs b/src/client.rs index dfc7da2b7..e47667f72 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, - sync::{mpsc, Arc, Mutex, RwLock}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -37,7 +37,6 @@ use hbb_common::{ }; pub use helper::LatencyController; pub use helper::*; -use scrap::Image; use scrap::{ codec::{Decoder, DecoderCfg}, VpxDecoderConfig, VpxVideoCodecId, @@ -47,7 +46,12 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; +pub mod io_loop; +pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); +pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); +pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); +pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. @@ -55,7 +59,23 @@ pub struct Client; #[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { -static ref AUDIO_HOST: Host = cpal::default_host(); + static ref AUDIO_HOST: Host = cpal::default_host(); +} +use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_key_state(key: enigo::Key) -> bool { + use enigo::KeyboardControllable; + #[cfg(target_os = "macos")] + if key == enigo::Key::NumLock { + return true; + } + ENIGO.lock().unwrap().get_key_state(key) } cfg_if::cfg_if! { @@ -846,8 +866,7 @@ impl VideoHandler { #[derive(Default)] pub struct LoginConfigHandler { id: String, - pub is_file_transfer: bool, - is_port_forward: bool, + pub conn_type: ConnType, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, @@ -886,12 +905,10 @@ impl LoginConfigHandler { /// # Arguments /// /// * `id` - id of peer - /// * `is_file_transfer` - Whether the connection is file transfer. - /// * `is_port_forward` - Whether the connection is port forward. - pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { + /// * `conn_type` - Connection type enum. + pub fn initialize(&mut self, id: String, conn_type: ConnType) { self.id = id; - self.is_file_transfer = is_file_transfer; - self.is_port_forward = is_port_forward; + self.conn_type = conn_type; let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; @@ -1048,7 +1065,8 @@ impl LoginConfigHandler { /// /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { - if self.is_port_forward || self.is_file_transfer { + if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) + { return None; } let mut n = 0; @@ -1094,7 +1112,8 @@ impl LoginConfigHandler { } pub fn get_option_message_after_login(&self) -> Option { - if self.is_port_forward || self.is_file_transfer { + if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) + { return None; } let mut n = 0; @@ -1260,13 +1279,13 @@ impl LoginConfigHandler { /// /// * `username` - The name of the peer. /// * `pi` - The peer info. - pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { + pub fn handle_peer_info(&mut self, pi: &PeerInfo) { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } - self.features = pi.features.into_option(); + self.features = pi.features.clone().into_option(); let serde = PeerInfoSerde { - username, + username: pi.username.clone(), hostname: pi.hostname.clone(), platform: pi.platform.clone(), }; @@ -1330,19 +1349,20 @@ impl LoginConfigHandler { version: crate::VERSION.to_string(), ..Default::default() }; - if self.is_file_transfer { - lr.set_file_transfer(FileTransfer { + match self.conn_type { + ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer { dir: self.get_remote_dir(), show_hidden: !self.get_option("remote_show_hidden").is_empty(), ..Default::default() - }); - } else if self.is_port_forward { - lr.set_port_forward(PortForward { + }), + ConnType::PORT_FORWARD => lr.set_port_forward(PortForward { host: self.port_forward.0.clone(), port: self.port_forward.1, ..Default::default() - }); + }), + _ => {} } + let mut msg_out = Message::new(); msg_out.set_login_request(lr); msg_out @@ -1651,6 +1671,12 @@ pub trait Interface: Send + Clone + 'static + Sized { fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); fn set_force_relay(&mut self, direct: bool, received: bool); + fn is_file_transfer(&self) -> bool; + fn is_port_forward(&self) -> bool; + fn is_rdp(&self) -> bool; + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err); + } fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index cc149c53f..d2f7b1648 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,4 +1,4 @@ -use hbb_common::{fs, message_proto::*}; +use hbb_common::{fs, message_proto::*, log}; use super::{Data, Interface}; @@ -114,4 +114,26 @@ pub trait FileManager: Interface { fn resume_job(&self, id: i32, is_remote: bool) { self.send(Data::ResumeJob((id, is_remote))); } + + fn set_confirm_override_file( + &self, + id: i32, + file_num: i32, + need_override: bool, + remember: bool, + is_upload: bool, + ) { + log::info!( + "confirm file transfer, job: {}, need_override: {}", + id, + need_override + ); + self.send(Data::SetConfirmOverrideFile(( + id, + file_num, + need_override, + remember, + is_upload, + ))); + } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs new file mode 100644 index 000000000..e61690c32 --- /dev/null +++ b/src/client/io_loop.rs @@ -0,0 +1,1208 @@ +use crate::client::{ + Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, + SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, +}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; + +use crate::ui_session_interface::{InvokeUi, Session}; +use crate::{client::Data, client::Interface}; + +use hbb_common::config::{PeerConfig, TransferSerde}; +use hbb_common::fs::{ + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + RemoveJobMeta, TransferJobMeta, +}; +use hbb_common::message_proto::permission_info::Permission; +use hbb_common::protobuf::Message as _; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, +}; +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, log, Stream}; +use std::collections::HashMap; + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +pub struct Remote { + handler: Session, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + old_clipboard: Arc>, + read_jobs: Vec, + write_jobs: Vec, + remove_jobs: HashMap, + timer: Interval, + last_update_jobs_status: (Instant, HashMap), + first_frame: bool, + #[cfg(windows)] + clipboard_file_context: Option>, + data_count: Arc, + frame_count: Arc, + video_format: CodecFormat, +} + +impl Remote { + pub fn new( + handler: Session, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + frame_count: Arc, + ) -> Self { + Self { + handler, + video_sender, + audio_sender, + receiver, + sender, + old_clipboard: Default::default(), + read_jobs: Vec::new(), + write_jobs: Vec::new(), + remove_jobs: Default::default(), + timer: time::interval(SEC30), + last_update_jobs_status: (Instant::now(), Default::default()), + first_frame: false, + #[cfg(windows)] + clipboard_file_context: None, + data_count: Arc::new(AtomicUsize::new(0)), + frame_count, + video_format: CodecFormat::Unknown, + } + } + + pub async fn io_loop(&mut self, key: &str, token: &str) { + let stop_clipboard = self.start_clipboard(); + let mut last_recv_time = Instant::now(); + let mut received = false; + let conn_type = if self.handler.is_file_transfer() { + ConnType::FILE_TRANSFER + } else { + ConnType::default() + }; + match Client::start( + &self.handler.id, + key, + token, + conn_type, + self.handler.clone(), + ) + .await + { + Ok((mut peer, direct)) => { + SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); + SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); + self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready + + // just build for now + #[cfg(not(windows))] + let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let mut rx_clip_client = get_rx_clip_client().lock().await; + + let mut status_timer = time::interval(Duration::new(1, 0)); + + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + log::error!("Connection closed: {}", err); + self.handler.set_force_relay(direct, received); + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + received = true; + self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); + if !self.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + if self.handler.is_restarting_remote_device() { + log::info!("Restart remote device"); + self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); + } else { + log::info!("Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer"); + } + break; + } + } + d = self.receiver.recv() => { + if let Some(d) = d { + if !self.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _msg = rx_clip_client.recv() => { + #[cfg(windows)] + match _msg { + Some((_, clip)) => { + allow_err!(peer.send(&clip_2_msg(clip)).await); + } + None => { + // unreachable!() + } + } + } + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + self.handler.msgbox("error", "Connection Error", "Timeout"); + break; + } + if !self.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + self.update_jobs_status(); + } else { + self.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + _ = status_timer.tick() => { + let speed = self.data_count.swap(0, Ordering::Relaxed); + let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); + let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; + self.handler.update_quality_status(QualityStatus { + speed:Some(speed), + fps:Some(fps), + ..Default::default() + }); + } + } + } + log::debug!("Exit io_loop of id={}", self.handler.id); + } + Err(err) => { + self.handler + .msgbox("error", "Connection Error", &err.to_string()); + } + } + if let Some(stop) = stop_clipboard { + stop.send(()).ok(); + } + SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); + SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(job) = self.remove_jobs.get_mut(&id) { + if job.no_confirm { + let file_num = (file_num + 1) as usize; + if file_num < job.files.len() { + let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); + self.sender + .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) + .ok(); + let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; + if elapsed >= 1000 { + job.last_update_job_status = Instant::now(); + } else { + return; + } + } else { + self.remove_jobs.remove(&id); + } + } + } + if let Some(err) = err { + self.handler.job_error(id, err, file_num); + } else { + self.handler.job_done(id, file_num); + } + } + + fn start_clipboard(&mut self) -> Option> { + if self.handler.is_file_transfer() || self.handler.is_port_forward() { + return None; + } + let (tx, rx) = std::sync::mpsc::channel(); + let old_clipboard = self.old_clipboard.clone(); + let tx_protobuf = self.sender.clone(); + let lc = self.handler.lc.clone(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + match ClipboardContext::new() { + Ok(mut ctx) => { + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&old_clipboard)); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit clipboard service of client"); + break; + } + _ => {} + } + if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) + || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + || lc.read().unwrap().disable_clipboard + { + continue; + } + if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { + tx_protobuf.send(Data::Message(msg)).ok(); + } + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + Some(tx) + } + + // TODO + fn load_last_jobs(&mut self) { + log::info!("start load last jobs"); + self.handler.clear_all_jobs(); + let pc = self.handler.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + // TODO: can add a confirm dialog + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + let job: Result = serde_json::from_str(&job_str); + if let Ok(job) = job { + self.handler.add_job( + cnt, + job.to.clone(), + job.remote.clone(), + job.file_num, + job.show_hidden, + false, + ); + cnt += 1; + println!("restore read_job: {:?}", job); + } + } + for job_str in pc.transfer.write_jobs.iter() { + let job: Result = serde_json::from_str(&job_str); + if let Ok(job) = job { + self.handler.add_job( + cnt, + job.remote.clone(), + job.to.clone(), + job.file_num, + job.show_hidden, + true, + ); + cnt += 1; + println!("restore write_job: {:?}", job); + } + } + self.handler.update_transfer_list(); + } + + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { + match data { + Data::Close => { + let mut misc = Misc::new(); + misc.set_close_reason("".to_owned()); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + return false; + } + Data::Login((password, remember)) => { + self.handler + .handle_login_from_ui(password, remember, peer) + .await; + } + Data::ToggleClipboardFile => { + self.check_clipboard_file_context(); + } + Data::Message(msg) => { + allow_err!(peer.send(&msg).await); + } + Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { + log::info!("send files, is remote {}", is_remote); + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!("New job {}, write to {} from remote {}", id, to, path); + self.write_jobs.push(fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + )); + allow_err!( + peer.send(&fs::new_send(id, path, file_num, include_hidden)) + .await + ); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(job) => { + log::debug!( + "New job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO + #[cfg(not(windows))] + let files = job.files().clone(); + #[cfg(windows)] + let mut files = job.files().clone(); + #[cfg(windows)] + if self.handler.peer_platform() != "Windows" { + // peer is not windows, need transform \ to / + fs::transform_windows_path(&mut files); + } + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); + } + } + } + } + Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!( + "new write waiting job {}, write to {} from remote {}", + id, + to, + path + ); + let mut job = fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + ); + job.is_last_job = true; + self.write_jobs.push(job); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(mut job) => { + log::debug!( + "new read waiting job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); + job.is_last_job = true; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + } + Data::ResumeJob((id, is_remote)) => { + if is_remote { + if let Some(job) = get_job(id, &mut self.write_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_send( + id, + job.remote.clone(), + job.file_num, + job.show_hidden + )) + .await + ); + } + } else { + if let Some(job) = get_job(id, &mut self.read_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + job.path.to_string_lossy().to_string(), + job.file_num, + job.files.clone() + )) + .await + ); + } + } + } + Data::SetNoConfirm(id) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + job.no_confirm = true; + } + } + Data::ConfirmDeleteFiles((id, file_num)) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + let i = file_num as usize; + if i < job.files.len() { + self.handler.ui_handler.confirm_delete_files( + id, + file_num, + job.files[i].name.clone(), + ); + self.handler.confirm_delete_files(id, file_num); + } + } + } + Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { + if is_upload { + if let Some(job) = fs::get_job(id, &mut self.read_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + job.confirm(&FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }); + } + } else { + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + let mut msg = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_send_confirm(FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }); + msg.set_file_action(file_action); + allow_err!(peer.send(&msg).await); + } + } + } + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { + let sep = self.handler.get_path_sep(is_remote); + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_all_files(ReadAllFiles { + id, + path: path.clone(), + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + self.remove_jobs + .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); + } else { + match fs::get_recursive_files(&path, include_hidden) { + Ok(entries) => { + // let m = make_fd(id, &entries, true); + // self.handler.call("updateFolderFiles", &make_args!(m)); + self.remove_jobs + .insert(id, RemoveJob::new(entries, path, sep, is_remote)); + } + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + } + } + } + Data::CancelJob(id) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id: id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.remove_download_file(); + fs::remove_job(id, &mut self.write_jobs); + } + fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + Data::RemoveDir((id, path)) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_dir(FileRemoveDir { + id, + path, + recursive: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } + Data::RemoveFile((id, path, file_num, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_file(FileRemoveFile { + id, + path, + file_num, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::remove_file(&path) { + Err(err) => { + self.handle_job_status(id, file_num, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, file_num, None); + } + } + } + } + Data::CreateDir((id, path, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_create(FileDirCreate { + id, + path, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::create_dir(&path) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, -1, None); + } + } + } + } + _ => {} + } + true + } + + #[inline] + fn update_job_status( + job: &fs::TransferJob, + elapsed: i32, + last_update_jobs_status: &mut (Instant, HashMap), + handler: &mut Session, + ) { + if elapsed <= 0 { + return; + } + let transferred = job.transferred(); + let last_transferred = { + if let Some(v) = last_update_jobs_status.1.get(&job.id()) { + v.to_owned() + } else { + 0 + } + }; + last_update_jobs_status.1.insert(job.id(), transferred); + let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); + } + + fn update_jobs_status(&mut self) { + let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; + if elapsed >= 1000 { + for job in self.read_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + pub async fn sync_jobs_status_to_local(&mut self) -> bool { + log::info!("sync transfer job status"); + let mut config: PeerConfig = self.handler.load_config(); + let mut transfer_metas = TransferSerde::default(); + for job in self.read_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.read_jobs.push(json_str); + } + for job in self.write_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.write_jobs.push(json_str); + } + log::info!("meta: {:?}", transfer_metas); + config.transfer = transfer_metas; + self.handler.save_config(config); + true + } + + async fn send_opts_after_login(&self, peer: &mut Stream) { + if let Some(opts) = self + .handler + .lc + .read() + .unwrap() + .get_option_message_after_login() + { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { + if let Ok(msg_in) = Message::parse_from_bytes(&data) { + match msg_in.union { + Some(message::Union::VideoFrame(vf)) => { + if !self.first_frame { + self.first_frame = true; + self.handler.close_success(); + self.handler.adapt_size(); + self.send_opts_after_login(peer).await; + } + let incomming_format = CodecFormat::from(&vf); + if self.video_format != incomming_format { + self.video_format = incomming_format.clone(); + self.handler.update_quality_status(QualityStatus { + codec_format: Some(incomming_format), + ..Default::default() + }) + }; + self.video_sender.send(MediaData::VideoFrame(vf)).ok(); + } + Some(message::Union::Hash(hash)) => { + self.handler + .handle_hash(&self.handler.password.clone(), hash, peer) + .await; + } + Some(message::Union::LoginResponse(lr)) => match lr.union { + Some(login_response::Union::Error(err)) => { + if !self.handler.handle_login_error(&err) { + return false; + } + } + Some(login_response::Union::PeerInfo(pi)) => { + self.handler.handle_peer_info(pi); + // self.check_clipboard_file_context(); + // if !(self.handler.is_file_transfer() + // || self.handler.is_port_forward() + // || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) + // || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + // || self.handler.lc.read().unwrap().disable_clipboard) + // { + // let txt = self.old_clipboard.lock().unwrap().clone(); + // if !txt.is_empty() { + // let msg_out = crate::create_clipboard_msg(txt); + // let sender = self.sender.clone(); + // tokio::spawn(async move { + // // due to clipboard service interval time + // sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + // sender.send(Data::Message(msg_out)).ok(); + // }); + // } + // } + + // if self.handler.is_file_transfer() { + // self.load_last_jobs().await; + // } + } + _ => {} + }, + Some(message::Union::CursorData(cd)) => { + self.handler.set_cursor_data(cd); + } + Some(message::Union::CursorId(id)) => { + self.handler.set_cursor_id(id.to_string()); + } + Some(message::Union::CursorPosition(cp)) => { + self.handler.set_cursor_position(cp); + } + Some(message::Union::Clipboard(cb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(cb, Some(&self.old_clipboard)); + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let content = if cb.compress { + hbb_common::compress::decompress(&cb.content) + } else { + cb.content.into() + }; + if let Ok(content) = String::from_utf8(content) { + self.handler.clipboard(content); + } + } + } + } + #[cfg(windows)] + Some(message::Union::Cliprdr(clip)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + if let Some(context) = &mut self.clipboard_file_context { + if let Some(clip) = msg_2_clip(clip) { + server_clip_file(context, 0, clip); + } + } + } + } + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::Dir(fd)) => { + #[cfg(windows)] + let entries = fd.entries.to_vec(); + #[cfg(not(windows))] + let mut entries = fd.entries.to_vec(); + #[cfg(not(windows))] + { + if self.handler.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + } + // let mut m = make_fd(fd.id, &entries, fd.id > 0); + // if fd.id <= 0 { + // m.set_item("path", fd.path); + // } + // self.handler.call("updateFolderFiles", &make_args!(m)); + if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { + log::info!("job set_files: {:?}", entries); + job.set_files(entries); + } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + job.files = entries; + } + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let read_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + ); + } + } + } + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let write_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + match fs::is_write_need_confirmation(&write_path, &digest) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let msg= new_send_confirm(FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::Skip(true)), + ..Default::default() + }); + allow_err!(peer.send(&msg).await); + } + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }, + ); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + ); + } + } + DigestCheckResult::NoSuchFile => { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }, + ); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error recving digest: {}", err); + } + } + } + } + } + } + Some(file_response::Union::Block(block)) => { + log::info!( + "file response block, file id:{}, file num: {}", + block.id, + block.file_num + ); + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block, None).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } + } + Some(file_response::Union::Done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::Error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + _ => {} + } + } + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::AudioFormat(f)) => { + self.audio_sender.send(MediaData::AudioFormat(f)).ok(); + } + Some(misc::Union::ChatMessage(c)) => { + self.handler.new_message(c.text); + } + Some(misc::Union::PermissionInfo(p)) => { + log::info!("Change permission {:?} -> {}", p.permission, p.enabled); + match p.permission.enum_value_or_default() { + Permission::Keyboard => { + SERVER_KEYBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + self.handler.set_permission("keyboard", p.enabled); + } + Permission::Clipboard => { + SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + self.handler.set_permission("clipboard", p.enabled); + } + Permission::Audio => { + self.handler.set_permission("audio", p.enabled); + } + Permission::File => { + SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); + if !p.enabled && self.handler.is_file_transfer() { + return true; + } + self.check_clipboard_file_context(); + self.handler.set_permission("file", p.enabled); + } + Permission::Restart => { + self.handler.set_permission("restart", p.enabled); + } + } + } + Some(misc::Union::SwitchDisplay(s)) => { + self.handler.ui_handler.switch_display(&s); + self.video_sender.send(MediaData::Reset).ok(); + if s.width > 0 && s.height > 0 { + self.handler.set_display(s.x, s.y, s.width, s.height); + } + } + Some(misc::Union::CloseReason(c)) => { + self.handler.msgbox("error", "Connection Error", &c); + return false; + } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } + _ => {} + }, + Some(message::Union::TestDelay(t)) => { + self.handler.handle_test_delay(t, peer).await; + } + Some(message::Union::AudioFrame(frame)) => { + if !self.handler.lc.read().unwrap().disable_audio { + self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); + } + } + Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::SendConfirm(c)) => { + if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { + job.confirm(&c); + } + } + _ => {} + }, + _ => {} + } + } + true + } + + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.handler.update_block_input_state(on); + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.handler + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.handler + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.handler.load_config(); + config.privacy_mode = on; + self.handler.save_config(config); + + self.handler.update_privacy_mode(); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.handler.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.handler + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.handler + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.handler + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + + fn check_clipboard_file_context(&mut self) { + #[cfg(windows)] + { + let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) + && self.handler.lc.read().unwrap().enable_file_transfer; + if enabled == self.clipboard_file_context.is_none() { + self.clipboard_file_context = if enabled { + match create_clipboard_file_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + Some(context) + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + None + } + } + } else { + log::info!("clipboard context for file transfer destroyed."); + None + }; + } + } + } +} + +struct RemoveJob { + files: Vec, + path: String, + sep: &'static str, + is_remote: bool, + no_confirm: bool, + last_update_job_status: Instant, +} + +impl RemoveJob { + fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { + Self { + files, + path, + sep, + is_remote, + no_confirm: false, + last_update_job_status: Instant::now(), + } + } + + pub fn _gen_meta(&self) -> RemoveJobMeta { + RemoveJobMeta { + path: self.path.clone(), + is_remote: self.is_remote, + no_confirm: self.no_confirm, + } + } +} diff --git a/src/flutter.rs b/src/flutter.rs index 6653f96f3..7316dd2ed 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,303 +1,31 @@ use std::{ collections::HashMap, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, - }, + sync::{Arc, RwLock}, }; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::config::{PeerConfig, TransferSerde}; -use hbb_common::fs::get_job; -use hbb_common::{ - allow_err, - compress::decompress, - config::{Config, LocalConfig}, - fs, - fs::{ - can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path, - DigestCheckResult, - }, - log, - message_proto::*, - protobuf::Message as _, - rendezvous_proto::ConnType, - tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, - }, - Stream, -}; +use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType}; -use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; +use crate::ui_session_interface::{io_loop, InvokeUi, Session}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; - -use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; -use enigo::{self, Enigo, KeyboardControllable}; -use rdev::{EventType::*, Key as RdevKey}; +use crate::{client::*, flutter_ffi::EventToUI}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; lazy_static::lazy_static! { - // static ref SESSION: Arc>> = Default::default(); - pub static ref SESSIONS: RwLock> = Default::default(); + pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel - pub static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); } -static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); - -// pub fn get_session<'a>(id: &str) -> Option<&'a Session> { -// SESSIONS.read().unwrap().get(id) -// } - -#[derive(Clone)] -pub struct Session { - id: String, - sender: Arc>>>, // UI to rust - lc: Arc>, - events2ui: Arc>>, +#[derive(Default, Clone)] +pub struct FlutterHandler { + pub event_stream: Arc>>>, } -impl Session { - /// Create a new remote session with the given id. - /// - /// # Arguments - /// - /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ - /// * `is_file_transfer` - If the session is used for file transfer. - pub fn start(identifier: &str, is_file_transfer: bool, events2ui: StreamSink) { - // TODO check same id - let session_id = get_session_id(identifier.to_owned()); - LocalConfig::set_remote_id(&session_id); - // TODO close - // Self::close(); - let events2ui = Arc::new(RwLock::new(events2ui)); - let session = Session { - id: session_id.clone(), - sender: Default::default(), - lc: Default::default(), - events2ui, - }; - session - .lc - .write() - .unwrap() - .initialize(session_id.clone(), is_file_transfer, false); - SESSIONS - .write() - .unwrap() - .insert(identifier.to_owned(), session.clone()); - std::thread::spawn(move || { - Connection::start(session, is_file_transfer); - }); - } - - /// Get the current session instance. - // pub fn get() -> Arc>> { - // SESSION.clone() - // } - - /// Get the option of the current session. - /// - /// # Arguments - /// - /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. - pub fn get_option(&self, name: &str) -> String { - if name == "remote_dir" { - return self.lc.read().unwrap().get_remote_dir(); - } - self.lc.read().unwrap().get_option(name) - } - - /// Set the option of the current session. - /// - /// # Arguments - /// - /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. - /// * `value` - The value of the option to set. - pub fn set_option(&self, name: String, value: String) { - let mut value = value; - let mut lc = self.lc.write().unwrap(); - if name == "remote_dir" { - value = lc.get_all_remote_dir(value); - } - lc.set_option(name, value); - } - - /// Input the OS password. - pub fn input_os_password(&self, pass: String, activate: bool) { - input_os_password(pass, activate, self.clone()); - } - - pub fn restart_remote_device(&self) { - let mut lc = self.lc.write().unwrap(); - lc.restarting_remote_device = true; - let msg = lc.restart_remote_device(); - self.send_msg(msg); - } - - /// Toggle an option. - pub fn toggle_option(&self, name: &str) { - let msg = self.lc.write().unwrap().toggle_option(name.to_owned()); - if let Some(msg) = msg { - self.send_msg(msg); - } - } - - /// Send a refresh command. - pub fn refresh(&self) { - self.send(Data::Message(LoginConfigHandler::refresh())); - } - - /// Get image quality. - pub fn get_image_quality(&self) -> String { - self.lc.read().unwrap().image_quality.clone() - } - - /// Set image quality. - pub fn set_image_quality(&self, value: &str) { - let msg = self - .lc - .write() - .unwrap() - .save_image_quality(value.to_owned()); - if let Some(msg) = msg { - self.send_msg(msg); - } - } - - /// Get the status of a toggle option. - /// Return `None` if the option is not found. - /// - /// # Arguments - /// - /// * `name` - The name of the option to get. - pub fn get_toggle_option(&self, name: &str) -> bool { - self.lc.write().unwrap().get_toggle_option(name) - } - - /// Login. - /// - /// # Arguments - /// - /// * `password` - The password to login. - /// * `remember` - If the password should be remembered. - pub fn login(&self, password: &str, remember: bool) { - self.send(Data::Login((password.to_owned(), remember))); - } - - /// Close the session. - pub fn close(&self) { - self.send(Data::Close); - } - - /// Reconnect to the current session. - pub fn reconnect(&self) { - self.send(Data::Close); - let session = self.clone(); - std::thread::spawn(move || { - Connection::start(session, false); - }); - } - - /// Get `remember` flag in [`LoginConfigHandler`]. - pub fn get_remember(&self) -> bool { - self.lc.read().unwrap().remember - } - - /// Send message over the current session. - /// - /// # Arguments - /// - /// * `msg` - The message to send. - #[inline] - pub fn send_msg(&self, msg: Message) { - self.send(Data::Message(msg)); - } - - pub fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { - // mode: legacy(0), map(1), translate(2), auto(3) - evt.mode = keyboard_mode.into(); - let mut msg_out = Message::new(); - msg_out.set_key_event(evt); - self.send(Data::Message(msg_out)); - } - - /// Send chat message over the current session. - /// - /// # Arguments - /// - /// * `text` - The message to send. - pub fn send_chat(&self, text: String) { - let mut misc = Misc::new(); - misc.set_chat_message(ChatMessage { - text, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send_msg(msg_out); - } - - // file trait - /// Send file over the current session. - // pub fn send_files( - // id: i32, - // path: String, - // to: String, - // file_num: i32, - // include_hidden: bool, - // is_remote: bool, - // ) { - // if let Some(session) = SESSION.write().unwrap().as_mut() { - // session.send_files(id, path, to, file_num, include_hidden, is_remote); - // } - // } - - // TODO into file trait - /// Confirm file override. - pub fn set_confirm_override_file( - &self, - id: i32, - file_num: i32, - need_override: bool, - remember: bool, - is_upload: bool, - ) { - log::info!( - "confirm file transfer, job: {}, need_override: {}", - id, - need_override - ); - self.send(Data::SetConfirmOverrideFile(( - id, - file_num, - need_override, - remember, - is_upload, - ))); - } - - /// Static method to send message over the current session. - /// - /// # Arguments - /// - /// * `msg` - The message to send. - // #[inline] - // pub fn send_msg_static(msg: Message) { - // if let Some(session) = SESSION.read().unwrap().as_ref() { - // session.send_msg(msg); - // } - // } - +impl FlutterHandler { /// Push an event to the event queue. /// An event is stored as json in the event queue. /// @@ -310,290 +38,54 @@ impl Session { assert!(h.get("name").is_none()); h.insert("name", name); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); - self.events2ui.read().unwrap().add(EventToUI::Event(out)); - } - - /// Get platform of peer. - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() - } - - /// Quick method for sending a ctrl_alt_del command. - pub fn ctrl_alt_del(&self) { - if self.peer_platform() == "Windows" { - let k = Key::ControlKey(ControlKey::CtrlAltDel); - self.key_down_or_up(1, k, false, false, false, false); - } else { - let k = Key::ControlKey(ControlKey::Delete); - self.key_down_or_up(3, k, true, true, false, false); + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Event(out)); } } +} - /// Switch the display. - /// - /// # Arguments - /// - /// * `display` - The display to switch to. - pub fn switch_display(&self, display: i32) { - let mut misc = Misc::new(); - misc.set_switch_display(SwitchDisplay { - display, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send_msg(msg_out); +impl InvokeUi for FlutterHandler { + fn set_cursor_data(&self, cd: CursorData) { + let colors = hbb_common::compress::decompress(&cd.colors); + self.push_event( + "cursor_data", + vec![ + ("id", &cd.id.to_string()), + ("hotx", &cd.hotx.to_string()), + ("hoty", &cd.hoty.to_string()), + ("width", &cd.width.to_string()), + ("height", &cd.height.to_string()), + ( + "colors", + &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), + ), + ], + ); } - /// Send lock screen command. - pub fn lock_screen(&self) { - let k = Key::ControlKey(ControlKey::LockScreen); - self.key_down_or_up(1, k, false, false, false, false); + fn set_cursor_id(&self, id: String) { + self.push_event("cursor_id", vec![("id", &id.to_string())]); } - /// Send key input command. - /// - /// # Arguments - /// - /// * `name` - The name of the key. - /// * `down` - Whether the key is down or up. - /// * `press` - If the key is simply being pressed(Down+Up). - /// * `alt` - If the alt key is also pressed. - /// * `ctrl` - If the ctrl key is also pressed. - /// * `shift` - If the shift key is also pressed. - /// * `command` - If the command key is also pressed. - pub fn input_key( - &self, - name: &str, - down: bool, - press: bool, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let chars: Vec = name.chars().collect(); - if chars.len() == 1 { - let key = Key::_Raw(chars[0] as _); - self._input_key(key, down, press, alt, ctrl, shift, command); - } else { - if let Some(key) = KEY_MAP.get(name) { - self._input_key(key.clone(), down, press, alt, ctrl, shift, command); - } - } + fn set_cursor_position(&self, cp: CursorPosition) { + self.push_event( + "cursor_position", + vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], + ); } - #[allow(dead_code)] - pub fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { - if self.get_key_state(enigo::Key::NumLock) { - return key; - } - match key { - RdevKey::Kp0 => RdevKey::Insert, - RdevKey::KpDecimal => RdevKey::Delete, - RdevKey::Kp1 => RdevKey::End, - RdevKey::Kp2 => RdevKey::DownArrow, - RdevKey::Kp3 => RdevKey::PageDown, - RdevKey::Kp4 => RdevKey::LeftArrow, - RdevKey::Kp5 => RdevKey::Clear, - RdevKey::Kp6 => RdevKey::RightArrow, - RdevKey::Kp7 => RdevKey::Home, - RdevKey::Kp8 => RdevKey::UpArrow, - RdevKey::Kp9 => RdevKey::PageUp, - _ => key, - } + /// unused in flutter, use switch_display or set_peer_info + fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32) {} + + fn update_privacy_mode(&self) { + self.push_event("update_privacy_mode", [].into()); } - pub fn get_key_state(&self, key: enigo::Key) -> bool { - #[cfg(target_os = "macos")] - if key == enigo::Key::NumLock { - return true; - } - ENIGO.lock().unwrap().get_key_state(key) + fn set_permission(&self, name: &str, value: bool) { + self.push_event("permission", vec![(name, &value.to_string())]); } - /// Map keyboard mode - pub fn input_raw_key(&self, keycode: i32, scancode: i32, down: bool){ - if scancode < 0 || keycode < 0{ - return; - } - let keycode: u32 = keycode as u32; - let scancode: u32 = scancode as u32; - - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_scancode(scancode) as RdevKey; - // Windows requires special handling - #[cfg(target_os = "windows")] - let key = rdev::get_win_key(keycode, scancode); - - let peer = self.peer_platform(); - - let mut key_event = KeyEvent::new(); - // According to peer platform. - let keycode: u32 = if peer == "Linux" { - rdev::linux_keycode_from_key(key).unwrap_or_default().into() - } else if peer == "Windows" { - #[cfg(not(windows))] - let key = self.convert_numpad_keys(key); - rdev::win_keycode_from_key(key).unwrap_or_default().into() - } else { - rdev::macos_keycode_from_key(key).unwrap_or_default().into() - }; - - key_event.set_chr(keycode); - key_event.down = down; - - if self.get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if self.get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - - self.send_key_event(key_event, KeyboardMode::Map); - } - - /// Input a string of text. - /// String is parsed into individual key presses. - /// - /// # Arguments - /// - /// * `value` - The text to input. TODO &str -> String - pub fn input_string(&self, value: &str) { - let mut key_event = KeyEvent::new(); - key_event.set_seq(value.to_owned()); - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - self.send_msg(msg_out); - } - - fn _input_key( - &self, - key: Key, - down: bool, - press: bool, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let v = if press { - 3 - } else if down { - 1 - } else { - 0 - }; - self.key_down_or_up(v, key, alt, ctrl, shift, command); - } - - pub fn send_mouse( - &self, - mask: i32, - x: i32, - y: i32, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - send_mouse(mask, x, y, alt, ctrl, shift, command, self); - } - - fn key_down_or_up( - &self, - down_or_up: i32, - key: Key, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let mut down_or_up = down_or_up; - let mut key_event = KeyEvent::new(); - match key { - Key::Chr(chr) => { - key_event.set_chr(chr); - } - Key::ControlKey(key) => { - key_event.set_control_key(key.clone()); - } - Key::_Raw(raw) => { - if raw > 'z' as u32 || raw < 'a' as u32 { - key_event.set_unicode(raw); - if down_or_up == 0 { - // ignore up, avoiding trigger twice - return; - } - down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side - } else { - // to make ctrl+c works on windows - key_event.set_chr(raw); - } - } - } - if alt { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command { - key_event.modifiers.push(ControlKey::Meta.into()); - } - if down_or_up == 1 { - key_event.down = true; - } else if down_or_up == 3 { - key_event.press = true; - } - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - // log::debug!("{:?}", msg_out); - self.send_msg(msg_out); - } - - pub fn load_config(&self) -> PeerConfig { - load_config(&self.id) - } - - pub fn save_config(&self, config: &PeerConfig) { - config.store(&self.id); - } - - pub fn get_platform(&self, is_remote: bool) -> String { - if is_remote { - self.lc.read().unwrap().info.platform.clone() - } else { - whoami::platform().to_string() - } - } - - pub fn load_last_jobs(&self) { - let pc = self.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - if !job_str.is_empty() { - self.push_event("load_last_job", vec![("value", job_str)]); - cnt += 1; - println!("restore read_job: {:?}", job_str); - } - } - for job_str in pc.transfer.write_jobs.iter() { - if !job_str.is_empty() { - self.push_event("load_last_job", vec![("value", job_str)]); - cnt += 1; - println!("restore write_job: {:?}", job_str); - } - } - } + fn close_success(&self) {} fn update_quality_status(&self, status: QualityStatus) { const NULL: String = String::new(); @@ -614,24 +106,111 @@ impl Session { ], ); } -} -impl FileManager for Session {} + fn set_connection_type(&self, is_secured: bool, direct: bool) { + self.push_event( + "connection_ready", + vec![ + ("secure", &is_secured.to_string()), + ("direct", &direct.to_string()), + ], + ); + } -#[async_trait] -impl Interface for Session { - fn send(&self, data: Data) { - if let Some(sender) = self.sender.read().unwrap().as_ref() { - sender.send(data).ok(); + fn job_error(&self, id: i32, err: String, file_num: i32) { + self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); + } + + fn job_done(&self, id: i32, file_num: i32) { + self.push_event( + "job_done", + vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], + ); + } + + fn clear_all_jobs(&self) { + // todo!() + } + + fn add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ) { + // todo!() + } + + fn update_transfer_list(&self) { + // todo!() + } + + fn confirm_delete_files(&self, id: i32, i: i32, name: String) { + // todo!() + } + + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + self.push_event( + "override_file_confirm", + vec![ + ("id", &id.to_string()), + ("file_num", &file_num.to_string()), + ("read_path", &to), + ("is_upload", &is_upload.to_string()), + ], + ); + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + self.push_event( + "job_progress", + vec![ + ("id", &id.to_string()), + ("file_num", &file_num.to_string()), + ("speed", &speed.to_string()), + ("finished_size", &finished_size.to_string()), + ], + ); + } + + fn adapt_size(&self) {} + + fn on_rgba(&self, data: &[u8]) { + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); } } - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { - let has_retry = if check_if_retry(msgtype, title, text) { - "true" - } else { - "" - }; + fn set_peer_info(&self, pi: &PeerInfo) { + let mut displays = Vec::new(); + for ref d in pi.displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + displays.push(h); + } + let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + self.push_event( + "peer_info", + vec![ + ("username", &pi.username), + ("hostname", &pi.hostname), + ("platform", &pi.platform), + ("sas_enabled", &pi.sas_enabled.to_string()), + ("displays", &displays), + ("version", &pi.version), + ("current_display", &pi.current_display.to_string()), + ], + ); + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + let has_retry = if retry { "true" } else { "" }; self.push_event( "msgbox", vec![ @@ -643,1092 +222,98 @@ impl Interface for Session { ); } - fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + fn new_message(&self, msg: String) { + self.push_event("chat_client_mode", vec![("text", &msg)]); } - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut lc = self.lc.write().unwrap(); - let username = lc.get_username(&pi); - let mut displays = Vec::new(); - let mut current = pi.current_display as usize; - - if lc.is_file_transfer { - if pi.username.is_empty() { - self.msgbox( - "error", - "Error", - "No active console user logged on, please connect and logon first.", - ); - return; - } - } else { - if pi.displays.is_empty() { - self.msgbox("error", "Remote Error", "No Display"); - } - for ref d in pi.displays.iter() { - let mut h: HashMap<&str, i32> = Default::default(); - h.insert("x", d.x); - h.insert("y", d.y); - h.insert("width", d.width); - h.insert("height", d.height); - displays.push(h); - } - if current >= pi.displays.len() { - current = 0; - } - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + fn switch_display(&self, display: &SwitchDisplay) { self.push_event( - "peer_info", + "switch_display", vec![ - ("username", &username), - ("hostname", &pi.hostname), - ("platform", &pi.platform), - ("sas_enabled", &pi.sas_enabled.to_string()), - ("displays", &displays), - ("version", &pi.version), - ("current_display", ¤t.to_string()), - ("is_file_transfer", &lc.is_file_transfer.to_string()), + ("display", &display.to_string()), + ("x", &display.x.to_string()), + ("y", &display.y.to_string()), + ("width", &display.width.to_string()), + ("height", &display.height.to_string()), ], ); - lc.handle_peer_info(username, pi); - let p = lc.should_auto_login(); - if !p.is_empty() { - input_os_password(p, true, self.clone()); - } } - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } - - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } - - async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), pass, hash, self, peer).await; - } - - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; - } - - async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - if !t.from_client { - self.update_quality_status(QualityStatus { - delay: Some(t.last_delay as _), - target_bitrate: Some(t.target_bitrate as _), - ..Default::default() - }); - handle_test_delay(t, peer).await; - } - } -} - -const MILLI1: Duration = Duration::from_millis(1); - -struct Connection { - video_handler: VideoHandler, - audio_handler: AudioHandler, - session: Session, - first_frame: bool, - read_jobs: Vec, - write_jobs: Vec, - timer: Interval, - last_update_jobs_status: (Instant, HashMap), - data_count: Arc, - frame_count: Arc, - video_format: CodecFormat, -} - -impl Connection { - // TODO: Similar to remote::start_clipboard - // merge the code - fn start_clipboard( - tx_protobuf: mpsc::UnboundedSender, - lc: Arc>, - ) -> Option> { - let (tx, rx) = std::sync::mpsc::channel(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - match ClipboardContext::new() { - Ok(mut ctx) => { - let old_clipboard: Arc> = Default::default(); - // ignore clipboard update before service start - check_clipboard(&mut ctx, Some(&old_clipboard)); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit clipboard service of client"); - break; - } - _ => {} - } - if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard - { - continue; - } - if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { - tx_protobuf.send(Data::Message(msg)).ok(); - } - }); - } - Err(err) => { - log::error!("Failed to start clipboard service of client: {}", err); - } - } - Some(tx) - } - - /// Create a new connection. - /// - /// # Arguments - /// - /// * `session` - The session to create a new connection for. - /// * `is_file_transfer` - Whether the connection is for file transfer. - #[tokio::main(flavor = "current_thread")] - async fn start(session: Session, is_file_transfer: bool) { - let mut last_recv_time = Instant::now(); - let (sender, mut receiver) = mpsc::unbounded_channel::(); - let mut stop_clipboard = None; - if !is_file_transfer { - stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); - } - *session.sender.write().unwrap() = Some(sender); - let conn_type = if is_file_transfer { - session.lc.write().unwrap().is_file_transfer = true; - ConnType::FILE_TRANSFER - } else { - ConnType::DEFAULT_CONN - }; - let latency_controller = LatencyController::new(); - let latency_controller_cl = latency_controller.clone(); - - let mut conn = Connection { - video_handler: VideoHandler::new(latency_controller), - audio_handler: AudioHandler::new(latency_controller_cl), - session: session.clone(), - first_frame: false, - read_jobs: Vec::new(), - write_jobs: Vec::new(), - timer: time::interval(SEC30), - last_update_jobs_status: (Instant::now(), Default::default()), - data_count: Arc::new(AtomicUsize::new(0)), - frame_count: Arc::new(AtomicUsize::new(0)), - video_format: CodecFormat::Unknown, - }; - let key = Config::get_option("key"); - let token = Config::get_option("access_token"); - - match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { - Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - - session.push_event( - "connection_ready", - vec![ - ("secure", &peer.is_secured().to_string()), - ("direct", &direct.to_string()), - ], - ); - - let mut status_timer = time::interval(Duration::new(1, 0)); - - loop { - tokio::select! { - res = peer.next() => { - if let Some(res) = res { - match res { - Err(err) => { - log::error!("Connection closed: {}", err); - session.msgbox("error", "Connection Error", &err.to_string()); - break; - } - Ok(ref bytes) => { - last_recv_time = Instant::now(); - conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed); - if !conn.handle_msg_from_peer(bytes, &mut peer).await { - break - } - } - } - } else { - if session.lc.read().unwrap().restarting_remote_device { - log::info!("Restart remote device"); - session.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); - } else { - log::info!("Reset by the peer"); - session.msgbox("error", "Connection Error", "Reset by the peer"); - } - break; - } - } - d = receiver.recv() => { - if let Some(d) = d { - if !conn.handle_msg_from_ui(d, &mut peer).await { - break; - } - } - } - _ = conn.timer.tick() => { - if last_recv_time.elapsed() >= SEC30 { - session.msgbox("error", "Connection Error", "Timeout"); - break; - } - if !conn.read_jobs.is_empty() { - if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { - log::debug!("Connection Error: {}", err); - break; - } - conn.update_jobs_status(); - } else { - conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); - } - } - _ = status_timer.tick() => { - let speed = conn.data_count.swap(0, Ordering::Relaxed); - let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _; - conn.session.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), - ..Default::default() - }); - } - } - } - log::debug!("Exit io_loop of id={}", session.id); - } - Err(err) => { - session.msgbox("error", "Connection Error", &err.to_string()); - } - } - - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - } - - /// Handle message from peer. - /// Return false if the connection should be closed. - /// - /// The message is handled by [`Message`], see [`message::Union`] for possible types. - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { - if let Ok(msg_in) = Message::parse_from_bytes(&data) { - match msg_in.union { - Some(message::Union::VideoFrame(vf)) => { - if !self.first_frame { - self.first_frame = true; - common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; - } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); - self.session.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), - ..Default::default() - }) - }; - if let Ok(true) = self.video_handler.handle_frame(vf) { - let stream = self.session.events2ui.read().unwrap(); - self.frame_count.fetch_add(1, Ordering::Relaxed); - stream.add(EventToUI::Rgba(ZeroCopyBuffer( - self.video_handler.rgb.clone(), - ))); - } - } - Some(message::Union::Hash(hash)) => { - self.session.handle_hash("", hash, peer).await; - } - Some(message::Union::LoginResponse(lr)) => match lr.union { - Some(login_response::Union::Error(err)) => { - if !self.session.handle_login_error(&err) { - return false; - } - } - Some(login_response::Union::PeerInfo(pi)) => { - self.session.handle_peer_info(pi); - } - _ => {} - }, - Some(message::Union::Clipboard(cb)) => { - if !self.session.lc.read().unwrap().disable_clipboard { - let content = if cb.compress { - decompress(&cb.content) - } else { - cb.content.into() - }; - if let Ok(content) = String::from_utf8(content) { - self.session - .push_event("clipboard", vec![("content", &content)]); - } - } - } - Some(message::Union::CursorData(cd)) => { - let colors = hbb_common::compress::decompress(&cd.colors); - self.session.push_event( - "cursor_data", - vec![ - ("id", &cd.id.to_string()), - ("hotx", &cd.hotx.to_string()), - ("hoty", &cd.hoty.to_string()), - ("width", &cd.width.to_string()), - ("height", &cd.height.to_string()), - ( - "colors", - &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), - ), - ], - ); - } - Some(message::Union::CursorId(id)) => { - self.session - .push_event("cursor_id", vec![("id", &id.to_string())]); - } - Some(message::Union::CursorPosition(cp)) => { - self.session.push_event( - "cursor_position", - vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], - ); - } - Some(message::Union::FileResponse(fr)) => { - match fr.union { - Some(file_response::Union::Dir(fd)) => { - let mut entries = fd.entries.to_vec(); - if self.session.peer_platform() == "Windows" { - transform_windows_path(&mut entries); - } - let id = fd.id; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], - ); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.set_files(entries); - } - } - Some(file_response::Union::Block(block)) => { - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job - } - self.update_jobs_status(); - } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); - } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); - } - } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }); - self.session.send_msg(msg); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); - self.session.send_msg(msg); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - write_path.to_string(), - false, - ); - } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }, - ); - self.session.send_msg(msg); - } - }, - Err(err) => { - println!("error recving digest: {}", err); - } - } - } - } - } - } - _ => {} - } - } - Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::AudioFormat(f)) => { - self.audio_handler.handle_format(f); // - } - Some(misc::Union::ChatMessage(c)) => { - self.session - .push_event("chat_client_mode", vec![("text", &c.text)]); - } - Some(misc::Union::PermissionInfo(p)) => { - log::info!("Change permission {:?} -> {}", p.permission, p.enabled); - use permission_info::Permission; - self.session.push_event( - "permission", - vec![( - match p.permission.enum_value_or_default() { - Permission::Keyboard => "keyboard", - Permission::Clipboard => "clipboard", - Permission::Audio => "audio", - Permission::Restart => "restart", - _ => "", - }, - &p.enabled.to_string(), - )], - ); - } - Some(misc::Union::SwitchDisplay(s)) => { - self.video_handler.reset(); - self.session.push_event( - "switch_display", - vec![ - ("display", &s.display.to_string()), - ("x", &s.x.to_string()), - ("y", &s.y.to_string()), - ("width", &s.width.to_string()), - ("height", &s.height.to_string()), - ], - ); - } - Some(misc::Union::CloseReason(c)) => { - self.session.msgbox("error", "Connection Error", &c); - return false; - } - Some(misc::Union::BackNotification(notification)) => { - if !self.handle_back_notification(notification).await { - return false; - } - } - _ => {} - }, - Some(message::Union::TestDelay(t)) => { - self.session.handle_test_delay(t, peer).await; - } - Some(message::Union::AudioFrame(frame)) => { - if !self.session.lc.read().unwrap().disable_audio { - self.audio_handler.handle_frame(frame); - } - } - Some(message::Union::FileAction(action)) => match action.union { - Some(file_action::Union::SendConfirm(c)) => { - if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); - } - } - _ => {} - }, - _ => {} - } - } - true - } - - async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { - match notification.union { - Some(back_notification::Union::BlockInputState(state)) => { - self.handle_back_msg_block_input( - state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), - ) - .await; - } - Some(back_notification::Union::PrivacyModeState(state)) => { - if !self - .handle_back_msg_privacy_mode( - state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), - ) - .await - { - return false; - } - } - _ => {} - } - true - } - - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - self.session.push_event( + fn update_block_input_state(&self, on: bool) { + self.push_event( "update_block_input_state", [("input_state", if on { "on" } else { "off" })].into(), ); } - async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { - match state { - back_notification::BlockInputState::BlkOnSucceeded => { - self.update_block_input_state(true); - } - back_notification::BlockInputState::BlkOnFailed => { - self.session - .msgbox("custom-error", "Block user input", "Failed"); - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffSucceeded => { - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffFailed => { - self.session - .msgbox("custom-error", "Unblock user input", "Failed"); - } - _ => {} - } + #[cfg(any(target_os = "android", target_os = "ios"))] + fn clipboard(&self, content: String) { + self.push_event("clipboard", vec![("content", &content)]); + } +} + +/// Create a new remote session with the given id. +/// +/// # Arguments +/// +/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +/// * `is_file_transfer` - If the session is used for file transfer. +/// * `is_port_forward` - If the session is used for port forward. +pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { + let session_id = get_session_id(id.to_owned()); + LocalConfig::set_remote_id(&session_id); + + let session: Session = Session { + id: session_id.clone(), + ..Default::default() + }; + + // TODO rdp + let conn_type = if is_file_transfer { + ConnType::FILE_TRANSFER + } else if is_port_forward { + ConnType::PORT_FORWARD + } else { + ConnType::DEFAULT_CONN + }; + + session + .lc + .write() + .unwrap() + .initialize(session_id, conn_type); + + if let Some(same_id_session) = SESSIONS + .write() + .unwrap() + .insert(id.to_owned(), session) + { + same_id_session.close(); } - #[inline(always)] - fn update_privacy_mode(&mut self, on: bool) { - let mut config = self.session.load_config(); - config.privacy_mode = on; - self.session.save_config(&config); - self.session.lc.write().unwrap().get_config().privacy_mode = on; - self.session.push_event("update_privacy_mode", [].into()); - } + Ok(()) +} - async fn handle_back_msg_privacy_mode( - &mut self, - state: back_notification::PrivacyModeState, - ) -> bool { - match state { - back_notification::PrivacyModeState::PrvOnByOther => { - self.session.msgbox( - "error", - "Connecting...", - "Someone turns on privacy mode, exit", - ); - return false; - } - back_notification::PrivacyModeState::PrvNotSupported => { - self.session - .msgbox("custom-error", "Privacy mode", "Unsupported"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnSucceeded => { - self.session - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); - self.update_privacy_mode(true); - } - back_notification::PrivacyModeState::PrvOnFailedDenied => { - self.session - .msgbox("custom-error", "Privacy mode", "Peer denied"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailedPlugin => { - self.session - .msgbox("custom-error", "Privacy mode", "Please install plugins"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailed => { - self.session - .msgbox("custom-error", "Privacy mode", "Failed"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffSucceeded => { - self.session - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffByPeer => { - self.session - .msgbox("custom-error", "Privacy mode", "Peer exit"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffFailed => { - self.session - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); - } - back_notification::PrivacyModeState::PrvOffUnknown => { - self.session - .msgbox("custom-error", "Privacy mode", "Turned off"); - // log::error!("Privacy mode is turned off with unknown reason"); - self.update_privacy_mode(false); - } - _ => {} - } - true - } - - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { - match data { - Data::Close => { - self.sync_jobs_status_to_local().await; - return false; - } - Data::Login((password, remember)) => { - self.session - .handle_login_from_ui(password, remember, peer) - .await; - } - Data::Message(msg) => { - allow_err!(peer.send(&msg).await); - } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); - if is_remote { - log::debug!("New job {}, write to {} from remote {}", id, to, path); - self.write_jobs.push(fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - )); - allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) - .await - ); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(job) => { - log::debug!( - "New job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd_flutter(id, job.files(), true); - self.session - .push_event("update_folder_files", vec![("info", &m)]); - let files = job.files().clone(); - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); - } - } - } - } - Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_all_files(ReadAllFiles { - id, - path: path.clone(), - include_hidden, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::get_recursive_files(&path, include_hidden) { - Ok(entries) => { - let mut fd = FileDirectory::new(); - fd.id = id; - fd.path = path; - fd.entries = entries; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "true")], - ); - } - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - } - } - } - Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); - } - fs::remove_job(id, &mut self.read_jobs); - } - Data::RemoveDir((id, path)) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_dir(FileRemoveDir { - id, - path, - recursive: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } - Data::RemoveFile((id, path, file_num, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_file(FileRemoveFile { - id, - path, - file_num, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::remove_file(&path) { - Err(err) => { - self.handle_job_status(id, file_num, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, file_num, None); - } - } - } - } - Data::CreateDir((id, path, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_create(FileDirCreate { - id, - path, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::create_dir(&path) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, -1, None); - } - } - } - } - Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { - if is_upload { - if let Some(job) = fs::get_job(id, &mut self.read_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - job.confirm(&FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - } - } else { - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - let mut msg = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - msg.set_file_action(file_action); - self.session.send_msg(msg); - } - } - } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); - if is_remote { - log::debug!( - "new write waiting job {}, write to {} from remote {}", - id, - to, - path - ); - let mut job = fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - ); - job.is_last_job = true; - self.write_jobs.push(job); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(mut job) => { - log::debug!( - "new read waiting job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd_flutter(job.id(), job.files(), true); - self.session - .push_event("update_folder_files", vec![("info", &m)]); - job.is_last_job = true; - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - } - } - } - } - Data::ResumeJob((id, is_remote)) => { - if is_remote { - if let Some(job) = get_job(id, &mut self.write_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_send( - id, - job.remote.clone(), - job.file_num, - job.show_hidden - )) - .await - ); - } - } else { - if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone() - )) - .await - ); - } - } - } - _ => {} - } - true - } - - #[inline] - fn update_job_status( - job: &fs::TransferJob, - elapsed: i32, - last_update_jobs_status: &mut (Instant, HashMap), - session: &Session, - ) { - if elapsed <= 0 { - return; - } - let transferred = job.transferred(); - let last_transferred = { - if let Some(v) = last_update_jobs_status.1.get(&job.id()) { - v.to_owned() - } else { - 0 - } - }; - last_update_jobs_status.1.insert(job.id(), transferred); - let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); - let file_num = job.file_num() - 1; - session.push_event( - "job_progress", - vec![ - ("id", &job.id().to_string()), - ("file_num", &file_num.to_string()), - ("speed", &speed.to_string()), - ("finished_size", &job.finished_size().to_string()), - ], - ); - } - - fn update_jobs_status(&mut self) { - let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; - if elapsed >= 1000 { - for job in self.read_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &self.session, - ); - } - for job in self.write_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &self.session, - ); - } - self.last_update_jobs_status.0 = Instant::now(); - } - } - - fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { - if let Some(err) = err { - self.session - .push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); - } else { - self.session.push_event( - "job_done", - vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], - ); - } - } - - fn handle_override_file_confirm( - &mut self, - id: i32, - file_num: i32, - read_path: String, - is_upload: bool, - ) { - self.session.push_event( - "override_file_confirm", - vec![ - ("id", &id.to_string()), - ("file_num", &file_num.to_string()), - ("read_path", &read_path), - ("is_upload", &is_upload.to_string()), - ], - ); - } - - async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); - let mut config: PeerConfig = self.session.load_config(); - let mut transfer_metas = TransferSerde::default(); - for job in self.read_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); - transfer_metas.read_jobs.push(json_str); - } - for job in self.write_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); - transfer_metas.write_jobs.push(json_str); - } - log::info!("meta: {:?}", transfer_metas); - config.transfer = transfer_metas; - self.session.save_config(&config); - true +/// start a session with the given id. +/// +/// # Arguments +/// +/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +/// * `events2ui` - The events channel to ui. +pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultType<()> { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + *session.event_stream.write().unwrap() = Some(event_stream); + let session = session.clone(); + std::thread::spawn(move || { + io_loop(session); + }); + Ok(()) + } else { + bail!("No session with peer id {}", id) } } // Server Side -// TODO connection_manager need use struct and trait,impl default method #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { use std::{ @@ -2360,3 +945,31 @@ pub fn get_session_id(id: String) -> String { id }; } + +// async fn start_one_port_forward( +// handler: Session, +// port: i32, +// remote_host: String, +// remote_port: i32, +// receiver: mpsc::UnboundedReceiver, +// key: &str, +// token: &str, +// ) { +// if let Err(err) = crate::port_forward::listen( +// handler.id.clone(), +// String::new(), // TODO +// port, +// handler.clone(), +// receiver, +// key, +// token, +// handler.lc.clone(), +// remote_host, +// remote_port, +// ) +// .await +// { +// handler.on_error(&format!("Failed to listen on {}: {}", port, err)); +// } +// log::info!("port forward (:{}) exit", port); +// } \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b2bbfb5c6..bc1335970 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,21 +13,22 @@ use hbb_common::{ }; use hbb_common::{password_security, ResultType}; -use crate::client::file_trait::FileManager; +use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}}; use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::flutter::{self, Session, SESSIONS}; +use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - check_super_user_permission, discover, forget_password, get_api_server, get_app_name, - get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, - get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, - get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, - set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, - store_fav, test_if_valid_server, update_temporary_password, using_public_server, + check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, + get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, + get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, + get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec, + has_rendezvous_service, post_request, set_local_option, set_option, set_options, + set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, + update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -107,13 +108,18 @@ pub fn host_stop_system_key_propagate(stopped: bool) { crate::platform::windows::stop_system_key_propagate(stopped); } -pub fn session_connect( - events2ui: StreamSink, - id: String, - is_file_transfer: bool, -) -> ResultType<()> { - Session::start(&id, is_file_transfer, events2ui); - Ok(()) +// FIXME: -> ResultType<()> cannot be parsed by frb_codegen +// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 +pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn { + if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { + SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_start(events2ui: StreamSink, id: String) -> ResultType<()> { + session_start_(&id, events2ui) } pub fn session_get_remember(id: String) -> Option { @@ -126,7 +132,7 @@ pub fn session_get_remember(id: String) -> Option { pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_toggle_option(&arg)) + Some(session.get_toggle_option(arg)) } else { None } @@ -137,17 +143,9 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn Option { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_image_quality()) - } else { - None - } -} - pub fn session_get_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_option(&arg)) + Some(session.get_option(arg)) } else { None } @@ -155,7 +153,7 @@ pub fn session_get_option(id: String, arg: String) -> Option { pub fn session_login(id: String, password: String, remember: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.login(&password, remember); + session.login(password, remember); } } @@ -168,7 +166,7 @@ pub fn session_close(id: String) { pub fn session_refresh(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.refresh(); + session.refresh_video(); } } @@ -179,14 +177,36 @@ pub fn session_reconnect(id: String) { } pub fn session_toggle_option(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.toggle_option(value); + } +} + +pub fn session_get_image_quality(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.toggle_option(&value); + Some(session.get_image_quality()) + } else { + None } } pub fn session_set_image_quality(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_image_quality(value); + } +} + +pub fn session_get_custom_image_quality(id: String) -> Option> { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.set_image_quality(&value); + Some(session.get_custom_image_quality()) + } else { + None + } +} + +pub fn session_set_custom_image_quality(id: String, value: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_custom_image_quality(value); } } @@ -208,11 +228,11 @@ pub fn session_switch_display(id: String, value: i32) { } } -pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){ - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.input_raw_key(keycode, scancode, down); - } -} +// pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){ +// if let Some(session) = SESSIONS.read().unwrap().get(&id) { +// session.input_raw_key(keycode, scancode, down); +// } +// } pub fn session_input_key( id: String, @@ -250,7 +270,7 @@ pub fn session_peer_option(id: String, name: String, value: String) { pub fn session_get_peer_option(id: String, name: String) -> String { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.get_option(&name); + return session.get_option(name); } "".to_string() } @@ -349,7 +369,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String { pub fn session_load_last_transfer_jobs(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.load_last_jobs(); + // return session.load_last_jobs(); } else { // a tip for flutter dev eprintln!( @@ -473,7 +493,7 @@ pub fn main_get_connect_status() -> String { pub fn main_check_connect_status() { #[cfg(not(any(target_os = "android", target_os = "ios")))] - check_connect_status(true); + check_mouse_time(); // avoid multi calls } pub fn main_is_using_public_server() -> bool { @@ -598,12 +618,32 @@ pub fn main_load_lan_peers() { { let data = HashMap::from([ ("name", "load_lan_peers".to_owned()), - ("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()), + ( + "peers", + serde_json::to_string(&get_lan_peers()).unwrap_or_default(), + ), ]); s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); }; } +pub fn session_add_port_forward( + id: String, + local_port: i32, + remote_host: String, + remote_port: i32, +) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.add_port_forward(local_port, remote_host, remote_port); + } +} + +pub fn session_remove_port_forward(id: String, local_port: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.remove_port_forward(local_port); + } +} + pub fn main_get_last_remote_id() -> String { // if !config::APP_DIR.read().unwrap().is_empty() { // res = LocalConfig::get_remote_id(); @@ -667,7 +707,6 @@ pub fn main_has_hwcodec() -> bool { has_hwcodec() } -// TODO pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); @@ -745,6 +784,14 @@ pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } +pub fn main_check_mouse_time() { + check_mouse_time(); +} + +pub fn main_get_mouse_time() -> f64 { + get_mouse_time() +} + pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 764868f35..ae55b9fab 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "正在重启远程设备"), ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"), ("Copied", "已复制"), + ("Exit Fullscreen", "退出全屏"), + ("Fullscreen", "全屏"), + ("Mobile Actions", "移动端操作"), + ("Select Monitor", "选择监视器"), + ("Control Actions", "控制操作"), + ("Display Settings", "显示设置"), + ("Ratio", "比例"), + ("Image Quality", "画质"), + ("Scroll Style", "滚屏方式"), + ("Show Menubar", "显示菜单栏"), + ("Hide Menubar", "隐藏菜单栏"), + ("Direct Connection", "直接连接"), + ("Relay Connection", "中继连接"), + ("Secure Connection", "安全连接"), + ("Insecure Connection", "非安全连接"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "适应窗口"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a05bb7c3d..4f49cb113 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Ukončete celou obrazovku"), + ("Fullscreen", "Celá obrazovka"), + ("Mobile Actions", "Mobilní akce"), + ("Select Monitor", "Vyberte možnost Monitor"), + ("Control Actions", "Ovládací akce"), + ("Display Settings", "Nastavení obrazovky"), + ("Ratio", "Poměr"), + ("Image Quality", "Kvalita obrazu"), + ("Scroll Style", "Štýl posúvania"), + ("Show Menubar", "Zobrazit panel nabídek"), + ("Hide Menubar", "skrýt panel nabídek"), + ("Direct Connection", "Přímé spojení"), + ("Relay Connection", "Připojení relé"), + ("Secure Connection", "Zabezpečené připojení"), + ("Insecure Connection", "Nezabezpečené připojení"), + ("Scale original", "Měřítko původní"), + ("Scale adaptive", "Měřítko adaptivní"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 941fff2c5..78db47875 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Afslut fuldskærm"), + ("Fullscreen", "Fuld skærm"), + ("Mobile Actions", "Mobile handlinger"), + ("Select Monitor", "Vælg Monitor"), + ("Control Actions", "Kontrolhandlinger"), + ("Display Settings", "Skærmindstillinger"), + ("Ratio", "Forhold"), + ("Image Quality", "Billede kvalitet"), + ("Scroll Style", "Rulstil"), + ("Show Menubar", "Vis menulinje"), + ("Hide Menubar", "skjul menulinjen"), + ("Direct Connection", "Direkte forbindelse"), + ("Relay Connection", "Relæforbindelse"), + ("Secure Connection", "Sikker forbindelse"), + ("Insecure Connection", "Usikker forbindelse"), + ("Scale original", "Skala original"), + ("Scale adaptive", "Skala adaptiv"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 3c8e43bac..60caecdd4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"), ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."), ("Copied", ""), + ("Exit Fullscreen", "Vollbild beenden"), + ("Fullscreen", "Ganzer Bildschirm"), + ("Mobile Actions", "Mobile Aktionen"), + ("Select Monitor", "Wählen Sie Überwachen aus"), + ("Control Actions", "Kontrollaktionen"), + ("Display Settings", "Bildschirmeinstellungen"), + ("Ratio", "Verhältnis"), + ("Image Quality", "Bildqualität"), + ("Scroll Style", "Scroll-Stil"), + ("Show Menubar", "Menüleiste anzeigen"), + ("Hide Menubar", "Menüleiste ausblenden"), + ("Direct Connection", "Direkte Verbindung"), + ("Relay Connection", "Relaisverbindung"), + ("Secure Connection", "Sichere Verbindung"), + ("Insecure Connection", "Unsichere Verbindung"), + ("Scale original", "Original skalieren"), + ("Scale adaptive", "Adaptiv skalieren"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 866bbb1a7..95fda5e90 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Eliru Plenekranon"), + ("Fullscreen", "Plenekrane"), + ("Mobile Actions", "Poŝtelefonaj Agoj"), + ("Select Monitor", "Elektu Monitoron"), + ("Control Actions", "Kontrolaj Agoj"), + ("Display Settings", "Montraj Agordoj"), + ("Ratio", "Proporcio"), + ("Image Quality", "Bilda Kvalito"), + ("Scroll Style", "Ruluma Stilo"), + ("Show Menubar", "Montru menubreton"), + ("Hide Menubar", "kaŝi menubreton"), + ("Direct Connection", "Rekta Konekto"), + ("Relay Connection", "Relajsa Konekto"), + ("Secure Connection", "Sekura Konekto"), + ("Insecure Connection", "Nesekura Konekto"), + ("Scale original", "Skalo originalo"), + ("Scale adaptive", "Skalo adapta"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 53852a051..17147a0cf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Reiniciando dispositivo remoto"), ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), ("Copied", ""), + ("Exit Fullscreen", "Salir de pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Acciones móviles"), + ("Select Monitor", "Seleccionar monitor"), + ("Control Actions", "Acciones de control"), + ("Display Settings", "Configuración de pantalla"), + ("Ratio", "Relación"), + ("Image Quality", "La calidad de imagen"), + ("Scroll Style", "Estilo de desplazamiento"), + ("Show Menubar", "ajustes de pantalla"), + ("Hide Menubar", "ocultar barra de menú"), + ("Direct Connection", "Conexión directa"), + ("Relay Connection", "Conexión de relé"), + ("Secure Connection", "Conexión segura"), + ("Insecure Connection", "Conexión insegura"), + ("Scale original", "escala originales"), + ("Scale adaptive", "Adaptable a escala"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 61c0c9a10..8df98de48 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Quitter le mode plein écran"), + ("Fullscreen", "Plein écran"), + ("Mobile Actions", "Actions mobiles"), + ("Select Monitor", "Sélectionnez Moniteur"), + ("Control Actions", "Actions de contrôle"), + ("Display Settings", "Paramètres d'affichage"), + ("Ratio", "Rapport"), + ("Image Quality", "Qualité d'image"), + ("Scroll Style", "Style de défilement"), + ("Show Menubar", "Afficher la barre de menus"), + ("Hide Menubar", "masquer la barre de menus"), + ("Direct Connection", "Connexion directe"), + ("Relay Connection", "Connexion relais"), + ("Secure Connection", "Connexion sécurisée"), + ("Insecure Connection", "Connexion non sécurisée"), + ("Scale original", "Échelle d'origine"), + ("Scale adaptive", "Échelle adaptative"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3f135380f..84d030eb0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Lépjen ki a teljes képernyőről"), + ("Fullscreen", "Teljes képernyő"), + ("Mobile Actions", "mobil műveletek"), + ("Select Monitor", "Válassza a Monitor lehetőséget"), + ("Control Actions", "Irányítási műveletek"), + ("Display Settings", "Megjelenítési beállítások"), + ("Ratio", "Hányados"), + ("Image Quality", "Képminőség"), + ("Scroll Style", "Görgetési stílus"), + ("Show Menubar", "Menüsor megjelenítése"), + ("Hide Menubar", "menüsor elrejtése"), + ("Direct Connection", "Közvetlen kapcsolat"), + ("Relay Connection", "Relé csatlakozás"), + ("Secure Connection", "Biztonságos kapcsolat"), + ("Insecure Connection", "Nem biztonságos kapcsolat"), + ("Scale original", "Eredeti méretarány"), + ("Scale adaptive", "Skála adaptív"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 39b07d976..a80dd6c22 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Keluar dari Layar Penuh"), + ("Fullscreen", "Layar penuh"), + ("Mobile Actions", "Tindakan Seluler"), + ("Select Monitor", "Pilih Monitor"), + ("Control Actions", "Tindakan Kontrol"), + ("Display Settings", "Pengaturan tampilan"), + ("Ratio", "Perbandingan"), + ("Image Quality", "Kualitas gambar"), + ("Scroll Style", "Gaya Gulir"), + ("Show Menubar", "Tampilkan bilah menu"), + ("Hide Menubar", "sembunyikan bilah menu"), + ("Direct Connection", "Koneksi langsung"), + ("Relay Connection", "Koneksi Relay"), + ("Secure Connection", "Koneksi aman"), + ("Insecure Connection", "Koneksi Tidak Aman"), + ("Scale original", "Skala asli"), + ("Scale adaptive", "Skala adaptif"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 45a7d45c6..8bf455d8e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -301,6 +301,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"), ("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"), ("remote_restarting_tip", "Riavviare il dispositivo remoto"), + ("Exit Fullscreen", "Esci dalla modalità schermo intero"), + ("Fullscreen", "A schermo intero"), + ("Mobile Actions", "Azioni mobili"), + ("Select Monitor", "Seleziona Monitora"), + ("Control Actions", "Azioni di controllo"), + ("Display Settings", "Impostazioni di visualizzazione"), + ("Ratio", "Rapporto"), + ("Image Quality", "Qualità dell'immagine"), + ("Scroll Style", "Stile di scorrimento"), + ("Show Menubar", "Mostra la barra dei menu"), + ("Hide Menubar", "nascondi la barra dei menu"), + ("Direct Connection", "Connessione diretta"), + ("Relay Connection", "Collegamento a relè"), + ("Secure Connection", "Connessione sicura"), + ("Insecure Connection", "Connessione insicura"), + ("Scale original", "Scala originale"), + ("Scale adaptive", "Scala adattiva"), ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 82d91993e..0c9b6c54d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -302,5 +302,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "本当に再起動しますか"), ("Restarting Remote Device", "リモート端末を再起動中"), ("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"), + ("Exit Fullscreen", "全画面表示を終了"), + ("Fullscreen", "全画面表示"), + ("Mobile Actions", "モバイル アクション"), + ("Select Monitor", "モニターを選択"), + ("Control Actions", "コントロール アクション"), + ("Display Settings", "ディスプレイの設定"), + ("Ratio", "比率"), + ("Image Quality", "画質"), + ("Scroll Style", "スクロール スタイル"), + ("Show Menubar", "メニューバーを表示"), + ("Hide Menubar", "メニューバーを隠す"), + ("Direct Connection", "直接接続"), + ("Relay Connection", "リレー接続"), + ("Secure Connection", "安全な接続"), + ("Insecure Connection", "安全でない接続"), + ("Scale original", "オリジナルサイズ"), + ("Scale adaptive", "フィットウィンドウ"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a0292adcb..44ba589f6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "정말로 재시작 하시겠습니까"), ("Restarting Remote Device", "원격 기기를 다시 시작하는중"), ("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."), + ("Exit Fullscreen", "전체 화면 종료"), + ("Fullscreen", "전체화면"), + ("Mobile Actions", "모바일 액션"), + ("Select Monitor", "모니터 선택"), + ("Control Actions", "제어 작업"), + ("Display Settings", "화면 설정"), + ("Ratio", "비율"), + ("Image Quality", "이미지 품질"), + ("Scroll Style", "스크롤 스타일"), + ("Show Menubar", "메뉴 표시줄 표시"), + ("Hide Menubar", "메뉴 표시줄 숨기기"), + ("Direct Connection", "직접 연결"), + ("Relay Connection", "릴레이 연결"), + ("Secure Connection", "보안 연결"), + ("Insecure Connection", "안전하지 않은 연결"), + ("Scale original", "원래 크기"), + ("Scale adaptive", "맞는 창"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 81eaddfaf..42bd49bbb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -303,5 +303,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set security password", "Ustaw hasło zabezpieczające"), ("Connection not allowed", "Połączenie niedozwolone"), ("Copied", ""), + ("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"), + ("Fullscreen", "Pełny ekran"), + ("Mobile Actions", "Działania mobilne"), + ("Select Monitor", "Wybierz Monitor"), + ("Control Actions", "Działania kontrolne"), + ("Display Settings", "Ustawienia wyświetlania"), + ("Ratio", "Stosunek"), + ("Image Quality", "Jakość obrazu"), + ("Scroll Style", "Styl przewijania"), + ("Show Menubar", "Pokaż pasek menu"), + ("Hide Menubar", "ukryj pasek menu"), + ("Direct Connection", "Bezpośrednie połączenie"), + ("Relay Connection", "Połączenie przekaźnika"), + ("Secure Connection", "Bezpieczne połączenie"), + ("Insecure Connection", "Niepewne połączenie"), + ("Scale original", "Skala oryginalna"), + ("Scale adaptive", "Skala adaptacyjna"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT b/src/lang/pt_PT.rs similarity index 95% rename from src/lang/pt_PT rename to src/lang/pt_PT.rs index e6e282575..e8c62d78a 100644 --- a/src/lang/pt_PT +++ b/src/lang/pt_PT.rs @@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"), ("Restarting Remote Device", "A reiniciar sistema remoto"), ("remote_restarting_tip", ""), + ("Exit Fullscreen", "Sair da tela cheia"), + ("Fullscreen", "Tela cheia"), + ("Mobile Actions", "Ações para celular"), + ("Select Monitor", "Selecionar monitor"), + ("Control Actions", "Ações de controle"), + ("Display Settings", "Configurações do visor"), + ("Ratio", "Razão"), + ("Image Quality", "Qualidade da imagem"), + ("Scroll Style", "Estilo de rolagem"), + ("Show Menubar", "Mostrar barra de menus"), + ("Hide Menubar", "ocultar barra de menu"), + ("Direct Connection", "Conexão direta"), + ("Relay Connection", "Conexão de relé"), + ("Secure Connection", "Conexão segura"), + ("Insecure Connection", "Conexão insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptável"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b98875d91..b9d3cad70 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", ""), + ("Fullscreen", ""), + ("Mobile Actions", ""), + ("Select Monitor", ""), + ("Control Actions", ""), + ("Display Settings", ""), + ("Ratio", ""), + ("Image Quality", ""), + ("Scroll Style", ""), + ("Show Menubar", ""), + ("Hide Menubar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), + ("Scale original", ""), + ("Scale adaptive", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index beb9f7fb9..4a0c8413c 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Перезагрузка удаленного устройства"), ("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."), ("Copied", ""), + ("Exit Fullscreen", "Выйти из полноэкранного режима"), + ("Fullscreen", "Полноэкранный"), + ("Mobile Actions", "Мобильные действия"), + ("Select Monitor", "Выберите монитор"), + ("Control Actions", "Действия по управлению"), + ("Display Settings", "Настройки отображения"), + ("Ratio", "Соотношение"), + ("Image Quality", "Качество изображения"), + ("Scroll Style", "Стиль прокрутки"), + ("Show Menubar", "Показать строку меню"), + ("Hide Menubar", "скрыть строку меню"), + ("Direct Connection", "Прямая связь"), + ("Relay Connection", "Релейное соединение"), + ("Secure Connection", "Безопасное соединение"), + ("Insecure Connection", "Небезопасное соединение"), + ("Scale original", "Оригинал масштаба"), + ("Scale adaptive", "Масштаб адаптивный"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 2a1a1f12f..1d083d2cc 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Ukončiť celú obrazovku"), + ("Fullscreen", "Celá obrazovka"), + ("Mobile Actions", "Mobilné akcie"), + ("Select Monitor", "Vyberte možnosť Monitor"), + ("Control Actions", "Kontrolné akcie"), + ("Display Settings", "Nastavenia displeja"), + ("Ratio", "Pomer"), + ("Image Quality", "Kvalita obrazu"), + ("Scroll Style", "Štýl posúvania"), + ("Show Menubar", "Zobraziť panel s ponukami"), + ("Hide Menubar", "skryť panel s ponukami"), + ("Direct Connection", "Priame pripojenie"), + ("Relay Connection", "Reléové pripojenie"), + ("Secure Connection", "Zabezpečené pripojenie"), + ("Insecure Connection", "Nezabezpečené pripojenie"), + ("Scale original", "Pôvodná mierka"), + ("Scale adaptive", "Prispôsobivá mierka"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index c7a22eb84..dcb2a9566 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", ""), + ("Fullscreen", ""), + ("Mobile Actions", ""), + ("Select Monitor", ""), + ("Control Actions", ""), + ("Display Settings", ""), + ("Ratio", ""), + ("Image Quality", ""), + ("Scroll Style", ""), + ("Show Menubar", ""), + ("Hide Menubar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), + ("Scale original", ""), + ("Scale adaptive", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2b79d43e1..01de89909 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Tam ekrandan çık"), + ("Fullscreen", "Tam ekran"), + ("Mobile Actions", "Mobil İşlemler"), + ("Select Monitor", "Monitörü Seç"), + ("Control Actions", "Kontrol Eylemleri"), + ("Display Settings", "Görüntü ayarları"), + ("Ratio", "Oran"), + ("Image Quality", "Görüntü kalitesi"), + ("Scroll Style", "Kaydırma Stili"), + ("Show Menubar", "Menü çubuğunu göster"), + ("Hide Menubar", "menü çubuğunu gizle"), + ("Direct Connection", "Doğrudan Bağlantı"), + ("Relay Connection", "Röle Bağlantısı"), + ("Secure Connection", "Güvenli bağlantı"), + ("Insecure Connection", "Güvenli Bağlantı"), + ("Scale original", "Orijinali ölçeklendir"), + ("Scale adaptive", "Ölçek uyarlanabilir"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 162ca6da8..84002b163 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "正在重啓遠程設備"), ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), ("Copied", "已複製"), + ("Exit Fullscreen", "退出全屏"), + ("Fullscreen", "全屏"), + ("Mobile Actions", "移動端操作"), + ("Select Monitor", "選擇監視器"), + ("Control Actions", "控制操作"), + ("Display Settings", "顯示設置"), + ("Ratio", "比例"), + ("Image Quality", "畫質"), + ("Scroll Style", "滾動樣式"), + ("Show Menubar", "顯示菜單欄"), + ("Hide Menubar", "隱藏菜單欄"), + ("Direct Connection", "直接連接"), + ("Relay Connection", "中繼連接"), + ("Secure Connection", "安全連接"), + ("Insecure Connection", "非安全連接"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "適應窗口"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 3266496b2..a9bc04946 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"), ("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"), ("Copied", ""), + ("Exit Fullscreen", "Thoát toàn màn hình"), + ("Fullscreen", "Toàn màn hình"), + ("Mobile Actions", "Hành động trên thiết bị di động"), + ("Select Monitor", "Chọn màn hình"), + ("Control Actions", "Kiểm soát hành động"), + ("Display Settings", "Thiết lập hiển thị"), + ("Ratio", "Tỉ lệ"), + ("Image Quality", "Chất lượng hình ảnh"), + ("Scroll Style", "Kiểu cuộn"), + ("Show Menubar", "Hiển thị thanh menu"), + ("Hide Menubar", "ẩn thanh menu"), + ("Direct Connection", "Kết nối trực tiếp"), + ("Relay Connection", "Kết nối chuyển tiếp"), + ("Secure Connection", "Kết nối an toàn"), + ("Insecure Connection", "Kết nối không an toàn"), + ("Scale original", "Quy mô gốc"), + ("Scale adaptive", "Quy mô thích ứng"), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index b7d1883c8..f554d447e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ mod port_forward; mod tray; mod ui_interface; +mod ui_session_interface; #[cfg(windows)] pub mod clipboard_file; diff --git a/src/port_forward.rs b/src/port_forward.rs index 9a697da42..934743edc 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use crate::client::*; use hbb_common::{ allow_err, bail, @@ -48,6 +50,9 @@ pub async fn listen( ui_receiver: mpsc::UnboundedReceiver, key: &str, token: &str, + lc: Arc>, + remote_host: String, + remote_port: i32, ) -> ResultType<()> { let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?; let addr = listener.local_addr()?; @@ -61,6 +66,7 @@ pub async fn listen( tokio::select! { Ok((forward, addr)) = listener.accept() => { log::info!("new connection from {:?}", addr); + lc.write().unwrap().port_forward = (remote_host.clone(), remote_port); let id = id.clone(); let password = password.clone(); let mut forward = Framed::new(forward, BytesCodec::new()); diff --git a/src/server/connection.rs b/src/server/connection.rs index 85b10a67d..e71b32e35 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -950,6 +950,7 @@ impl Connection { addr )) .await; + return false; } } } diff --git a/src/ui.rs b/src/ui.rs index 78654e9ec..b66d1453b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -146,7 +146,7 @@ pub fn start(args: &mut [String]) { let args: Vec = iter.map(|x| x.clone()).collect(); frame.set_title(&id); frame.register_behavior("native-remote", move || { - Box::new(remote::Handler::new( + Box::new(remote::SciterSession::new( cmd.clone(), id.clone(), pass.clone(), diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 6100c8b87..91dd08fdc 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,9 +1,9 @@ use std::{ - collections::{HashMap, HashSet}, - ops::Deref, + collections::HashMap, + ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, + atomic::{AtomicBool, Ordering}, + Arc, Mutex, }, }; @@ -22,101 +22,250 @@ use clipboard::{ cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context, get_rx_clip_client, server_clip_file, }; -use enigo::{self, Enigo, KeyboardControllable}; -use hbb_common::{ - allow_err, - config::{Config, LocalConfig, PeerConfig, TransferSerde}, - fs::{ - self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, - DigestCheckResult, RemoveJobMeta, TransferJobMeta, - }, - get_version_number, log, - message_proto::{permission_info::Permission, *}, - protobuf::Message as _, - rendezvous_proto::ConnType, - sleep, - tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, - }, - Stream, -}; -use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; + +use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; #[cfg(windows)] use crate::clipboard_file::*; use crate::{ client::*, - common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::{InvokeUi, Session, IS_IN}, }; -use errno; type Video = AssetPtr; lazy_static::lazy_static! { - static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); static ref VIDEO: Arc>> = Default::default(); - static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); - static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); } -fn get_key_state(key: enigo::Key) -> bool { - #[cfg(target_os = "macos")] - if key == enigo::Key::NumLock { - return true; - } - ENIGO.lock().unwrap().get_key_state(key) -} - -static IS_IN: AtomicBool = AtomicBool::new(false); -static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); #[cfg(windows)] static mut IS_ALT_GR: bool = false; -#[derive(Default)] -pub struct HandlerInner { - element: Option, - sender: Option>, - thread: Option>, +/// SciterHandler +/// * element +/// * thread TODO check if flutter need +/// * close_state for file path when close +#[derive(Clone, Default)] +pub struct SciterHandler { + element: Arc>>, close_state: HashMap, } -#[derive(Clone, Default)] -pub struct Handler { - inner: Arc>, - cmd: String, - id: String, - password: String, - args: Vec, - lc: Arc>, -} +impl SciterHandler { + #[inline] + fn call(&self, func: &str, args: &[Value]) { + if let Some(ref e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, args)); + } + } -impl Deref for Handler { - type Target = Arc>; - - fn deref(&self) -> &Self::Target { - &self.inner + #[inline] + fn call2(&self, func: &str, args: &[Value]) { + if let Some(ref e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } } } -impl FileManager for Handler {} +impl InvokeUi for SciterHandler { + fn set_cursor_data(&self, cd: CursorData) { + let mut colors = hbb_common::compress::decompress(&cd.colors); + if colors.iter().filter(|x| **x != 0).next().is_none() { + log::info!("Fix transparent"); + // somehow all 0 images shows black rect, here is a workaround + colors[3] = 1; + } + let mut png = Vec::new(); + if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { + self.call( + "setCursorData", + &make_args!( + cd.id.to_string(), + cd.hotx, + cd.hoty, + cd.width, + cd.height, + &png[..] + ), + ); + } + } -impl sciter::EventHandler for Handler { + fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { + self.call("setDisplay", &make_args!(x, y, w, h)); + // https://sciter.com/forums/topic/color_spaceiyuv-crash + // Nothing spectacular in decoder – done on CPU side. + // So if you can do BGRA translation on your side – the better. + // BGRA is used as internal image format so it will not require additional transformations. + VIDEO.lock().unwrap().as_mut().map(|v| { + v.stop_streaming().ok(); + let ok = v.start_streaming((w, h), COLOR_SPACE::Rgb32, None); + log::info!("[video] reinitialized: {:?}", ok); + }); + } + + fn update_privacy_mode(&self) { + self.call("updatePrivacyMode", &[]); + } + + fn set_permission(&self, name: &str, value: bool) { + self.call2("setPermission", &make_args!(name, value)); + } + + fn close_success(&self) { + self.call2("closeSuccess", &make_args!()); + } + + fn update_quality_status(&self, status: QualityStatus) { + self.call2( + "updateQualityStatus", + &make_args!( + status.speed.map_or(Value::null(), |it| it.into()), + status.fps.map_or(Value::null(), |it| it.into()), + status.delay.map_or(Value::null(), |it| it.into()), + status.target_bitrate.map_or(Value::null(), |it| it.into()), + status + .codec_format + .map_or(Value::null(), |it| it.to_string().into()) + ), + ); + } + + fn set_cursor_id(&self, id: String) { + self.call("setCursorId", &make_args!(id)); + } + + fn set_cursor_position(&self, cp: CursorPosition) { + self.call("setCursorPosition", &make_args!(cp.x, cp.y)); + } + + fn set_connection_type(&self, is_secured: bool, direct: bool) { + self.call("setConnectionType", &make_args!(is_secured, direct)); + } + + fn job_error(&self, id: i32, err: String, file_num: i32) { + self.call("jobError", &make_args!(id, err, file_num)); + } + + fn job_done(&self, id: i32, file_num: i32) { + self.call("jobDone", &make_args!(id, file_num)); + } + + fn clear_all_jobs(&self) { + self.call("clearAllJobs", &make_args!()); + } + + fn add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ) { + todo!() + } + + fn update_transfer_list(&self) { + self.call("updateTransferList", &make_args!()); + } + + fn confirm_delete_files(&self, id: i32, i: i32, name: String) { + self.call("confirmDeleteFiles", &make_args!(id, i, name)); + } + + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + self.call( + "overrideFileConfirm", + &make_args!(id, file_num, to, is_upload), + ); + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + self.call( + "jobProgress", + &make_args!(id, file_num, speed, finished_size), + ); + } + + fn adapt_size(&self) { + self.call("adaptSize", &make_args!()); + } + + fn on_rgba(&self, data: &[u8]) { + VIDEO + .lock() + .unwrap() + .as_mut() + .map(|v| v.render_frame(data).ok()); + } + + fn set_peer_info(&self, pi: &PeerInfo) { + let mut pi_sciter = Value::map(); + pi_sciter.set_item("username", pi.username.clone()); + pi_sciter.set_item("hostname", pi.hostname.clone()); + pi_sciter.set_item("platform", pi.platform.clone()); + pi_sciter.set_item("sas_enabled", pi.sas_enabled); + + let mut displays = Value::array(0); + for ref d in pi.displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + displays.push(display); + } + pi_sciter.set_item("displays", displays); + pi_sciter.set_item("current_display", pi.current_display); + self.call("updatePi", &make_args!(pi_sciter)); + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); + } + + fn new_message(&self, msg: String) { + self.call("newMessage", &make_args!(msg)); + } + + fn switch_display(&self, display: &SwitchDisplay) { + self.call("switchDisplay", &make_args!(display.display)); + } + + fn update_block_input_state(&self, on: bool) { + self.call("updateBlockInputState", &make_args!(on)); + } +} + +pub struct SciterSession(Session); + +impl Deref for SciterSession { + type Target = Session; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SciterSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl sciter::EventHandler for SciterSession { fn get_subscription(&mut self) -> Option { Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) } fn attached(&mut self, root: HELEMENT) { - self.write().unwrap().element = Some(Element::from(root)); + *self.element.lock().unwrap() = Some(Element::from(root)); } fn detached(&mut self, _root: HELEMENT) { - self.write().unwrap().element = None; - self.write().unwrap().sender.take().map(|sender| { + *self.element.lock().unwrap() = None; + self.sender.write().unwrap().take().map(|sender| { sender.send(Data::Close).ok(); }); } @@ -244,116 +393,29 @@ impl sciter::EventHandler for Handler { } } -impl Handler { +impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { - let me = Self { - cmd, + let session: Session = Session { + cmd: cmd.clone(), id: id.clone(), password: password.clone(), args, ..Default::default() }; - me.lc - .write() - .unwrap() - .initialize(id, me.is_file_transfer(), me.is_port_forward()); - me - } - fn update_quality_status(&self, status: QualityStatus) { - self.call2( - "updateQualityStatus", - &make_args!( - status.speed.map_or(Value::null(), |it| it.into()), - status.fps.map_or(Value::null(), |it| it.into()), - status.delay.map_or(Value::null(), |it| it.into()), - status.target_bitrate.map_or(Value::null(), |it| it.into()), - status - .codec_format - .map_or(Value::null(), |it| it.to_string().into()) - ), - ); - } + let conn_type = if cmd.eq("--file-transfer") { + ConnType::FILE_TRANSFER + } else if cmd.eq("--port-forward") { + ConnType::PORT_FORWARD + } else if cmd.eq("--rdp") { + ConnType::RDP + } else { + ConnType::DEFAULT_CONN + }; - fn start_keyboard_hook(&self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - let mut me = self.clone(); - #[cfg(windows)] - crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); - std::thread::spawn(move || { - // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); - lazy_static::lazy_static! { - static ref MUTEX_SPECIAL_KEYS: Mutex> = { - let mut m = HashMap::new(); - m.insert(RdevKey::ShiftLeft, false); - m.insert(RdevKey::ShiftRight, false); - m.insert(RdevKey::ControlLeft, false); - m.insert(RdevKey::ControlRight, false); - m.insert(RdevKey::Alt, false); - m.insert(RdevKey::AltGr, false); - m.insert(RdevKey::MetaLeft, false); - m.insert(RdevKey::MetaRight, false); - Mutex::new(m) - }; - } + session.lc.write().unwrap().initialize(id, conn_type); - let func = move |evt: Event| { - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - let (_key, down) = match evt.event_type { - KeyPress(k) => { - // keyboard long press - if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { - if *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&k).unwrap() { - return; - } - MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); - } - (k, true) - } - KeyRelease(k) => { - // keyboard long press - if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { - MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); - } - (k, false) - } - _ => return, - }; - - me.key_down_or_up(down, _key, evt); - }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); - } - - fn get_view_style(&mut self) -> String { - return self.lc.read().unwrap().view_style.clone(); - } - - fn get_image_quality(&mut self) -> String { - return self.lc.read().unwrap().image_quality.clone(); - } - - fn get_keyboard_mode(&mut self) -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")) - .to_lowercase(); - } - - fn save_keyboard_mode(&mut self, value: String) { - std::env::set_var("KEYBOARD_MODE", value); + Self(session) } fn get_custom_image_quality(&mut self) -> Value { @@ -364,87 +426,6 @@ impl Handler { v } - #[inline] - pub(super) fn save_config(&self, config: PeerConfig) { - self.lc.write().unwrap().save_config(config); - } - - fn save_view_style(&mut self, value: String) { - self.lc.write().unwrap().save_view_style(value); - } - - #[inline] - pub(super) fn load_config(&self) -> PeerConfig { - load_config(&self.id) - } - - fn toggle_option(&mut self, name: String) { - let msg = self.lc.write().unwrap().toggle_option(name.clone()); - if name == "enable-file-transfer" { - self.send(Data::ToggleClipboardFile); - } - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - - fn get_toggle_option(&mut self, name: String) -> bool { - self.lc.read().unwrap().get_toggle_option(&name) - } - - fn is_privacy_mode_supported(&self) -> bool { - self.lc.read().unwrap().is_privacy_mode_supported() - } - - fn refresh_video(&mut self) { - self.send(Data::Message(LoginConfigHandler::refresh())); - } - - fn save_custom_image_quality(&mut self, custom_image_quality: i32) { - let msg = self - .lc - .write() - .unwrap() - .save_custom_image_quality(custom_image_quality); - self.send(Data::Message(msg)); - } - - fn save_image_quality(&mut self, value: String) { - let msg = self.lc.write().unwrap().save_image_quality(value); - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - - fn get_remember(&mut self) -> bool { - self.lc.read().unwrap().remember - } - - fn set_write_override( - &mut self, - job_id: i32, - file_num: i32, - is_override: bool, - remember: bool, - is_upload: bool, - ) -> bool { - self.send(Data::SetConfirmOverrideFile(( - job_id, - file_num, - is_override, - remember, - is_upload, - ))); - true - } - - fn has_hwcodec(&self) -> bool { - #[cfg(not(feature = "hwcodec"))] - return false; - #[cfg(feature = "hwcodec")] - return true; - } - fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { @@ -469,56 +450,11 @@ impl Handler { } } - fn change_prefer_codec(&self) { - let msg = self.lc.write().unwrap().change_prefer_codec(); - self.send(Data::Message(msg)); - } - - fn restart_remote_device(&mut self) { - let mut lc = self.lc.write().unwrap(); - lc.restarting_remote_device = true; - let msg = lc.restart_remote_device(); - self.send(Data::Message(msg)); - } - - pub fn is_restarting_remote_device(&self) -> bool { - self.lc.read().unwrap().restarting_remote_device - } - - fn t(&self, name: String) -> String { - crate::client::translate(name) - } - - fn get_audit_server(&self) -> String { - if self.lc.read().unwrap().conn_id <= 0 - || LocalConfig::get_option("access_token").is_empty() - { - return "".to_owned(); - } - crate::get_audit_server( - Config::get_option("api-server"), - Config::get_option("custom-rendezvous-server"), - ) - } - - fn send_note(&self, note: String) { - let url = self.get_audit_server(); - let id = self.id.clone(); - let conn_id = self.lc.read().unwrap().conn_id; - std::thread::spawn(move || { - send_note(url, id, conn_id, note); - }); - } - - fn is_xfce(&self) -> bool { - crate::platform::is_xfce() - } - fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { let size = (x, y, w, h); let mut config = self.load_config(); if self.is_file_transfer() { - let close_state = self.read().unwrap().close_state.clone(); + let close_state = self.close_state.clone(); let mut has_change = false; for (k, mut v) in close_state { if k == "remote_dir" { @@ -574,34 +510,6 @@ impl Handler { v } - fn remove_port_forward(&mut self, port: i32) { - let mut config = self.load_config(); - config.port_forwards = config - .port_forwards - .drain(..) - .filter(|x| x.0 != port) - .collect(); - self.save_config(config); - self.send(Data::RemovePortForward(port)); - } - - fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { - let mut config = self.load_config(); - if config - .port_forwards - .iter() - .filter(|x| x.0 == port) - .next() - .is_some() - { - return; - } - let pf = (port, remote_host, remote_port); - config.port_forwards.push(pf.clone()); - self.save_config(config); - self.send(Data::AddPortForward(pf)); - } - fn get_size(&mut self) -> Value { let s = if self.is_file_transfer() { self.lc.read().unwrap().size_ft @@ -618,10 +526,6 @@ impl Handler { v } - fn get_id(&mut self) -> String { - self.id.clone() - } - fn get_default_pi(&mut self) -> Value { let mut pi = Value::map(); let info = self.lc.read().unwrap().info.clone(); @@ -631,155 +535,8 @@ impl Handler { pi } - fn get_option(&self, k: String) -> String { - self.lc.read().unwrap().get_option(&k) - } - - fn set_option(&self, k: String, v: String) { - self.lc.write().unwrap().set_option(k, v); - } - - fn input_os_password(&mut self, pass: String, activate: bool) { - input_os_password(pass, activate, self.clone()); - } - - fn save_close_state(&self, k: String, v: String) { - self.write().unwrap().close_state.insert(k, v); - } - - fn get_chatbox(&mut self) -> String { - #[cfg(feature = "inline")] - return super::inline::get_chatbox(); - #[cfg(not(feature = "inline"))] - return "".to_owned(); - } - - fn get_icon(&mut self) -> String { - crate::get_icon() - } - - fn send_chat(&mut self, text: String) { - let mut misc = Misc::new(); - misc.set_chat_message(ChatMessage { - text, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); - } - - fn switch_display(&mut self, display: i32) { - let mut misc = Misc::new(); - misc.set_switch_display(SwitchDisplay { - display, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); - } - - fn is_file_transfer(&self) -> bool { - self.cmd == "--file-transfer" - } - - fn is_port_forward(&self) -> bool { - self.cmd == "--port-forward" || self.is_rdp() - } - - fn is_rdp(&self) -> bool { - self.cmd == "--rdp" - } - - fn reconnect(&mut self) { - println!("reconnecting"); - let cloned = self.clone(); - let mut lock = self.write().unwrap(); - lock.thread.take().map(|t| t.join()); - lock.thread = Some(std::thread::spawn(move || { - io_loop(cloned); - })); - } - - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() - } - - fn get_platform(&mut self, is_remote: bool) -> String { - if is_remote { - self.peer_platform() - } else { - whoami::platform().to_string() - } - } - - fn get_path_sep(&mut self, is_remote: bool) -> &'static str { - let p = self.get_platform(is_remote); - if &p == "Windows" { - return "\\"; - } else { - return "/"; - } - } - - fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { - let mut path = Config::icon_path(); - if file_type == FileType::DirLink as i32 { - let new_path = path.join("dir_link"); - if !std::fs::metadata(&new_path).is_ok() { - #[cfg(windows)] - allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - #[cfg(not(windows))] - allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - } - path = new_path; - } else if file_type == FileType::File as i32 { - if !ext.is_empty() { - path = path.join(format!("file.{}", ext)); - } else { - path = path.join("file"); - } - if !std::fs::metadata(&path).is_ok() { - allow_err!(std::fs::File::create(&path)); - } - } else if file_type == FileType::FileLink as i32 { - let new_path = path.join("file_link"); - if !std::fs::metadata(&new_path).is_ok() { - path = path.join("file"); - if !std::fs::metadata(&path).is_ok() { - allow_err!(std::fs::File::create(&path)); - } - #[cfg(windows)] - allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - #[cfg(not(windows))] - allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - } - path = new_path; - } else if file_type == FileType::DirDrive as i32 { - if cfg!(windows) { - path = fs::get_path("C:"); - } else if cfg!(target_os = "macos") { - if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { - for entry in entries { - if let Ok(entry) = entry { - path = entry.path(); - break; - } - } - } - } - } - fs::get_string(&path) - } - - fn login(&mut self, password: String, remember: bool) { - self.send(Data::Login((password, remember))); - } - - fn new_rdp(&mut self) { - self.send(Data::NewRDP); + fn save_close_state(&mut self, k: String, v: String) { + self.close_state.insert(k, v); } fn enter(&mut self) { @@ -789,69 +546,11 @@ impl Handler { } fn leave(&mut self) { - for key in TO_RELEASE.lock().unwrap().iter() { - self.map_keyboard_mode(false, *key, None) - } #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(false); IS_IN.store(false, Ordering::SeqCst); } - fn send_mouse( - &mut self, - mask: i32, - x: i32, - y: i32, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - #[allow(unused_mut)] - let mut command = command; - #[cfg(windows)] - { - if !command && crate::platform::windows::get_win_key_state() { - command = true; - } - } - - send_mouse(mask, x, y, alt, ctrl, shift, command, self); - // on macos, ctrl + left button down = right button down, up won't emit, so we need to - // emit up myself if peer is not macos - // to-do: how about ctrl + left from win to macos - if cfg!(target_os = "macos") { - let buttons = mask >> 3; - let evt_type = mask & 0x7; - if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { - self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); - } - } - } - - fn set_cursor_data(&mut self, cd: CursorData) { - let mut colors = hbb_common::compress::decompress(&cd.colors); - if colors.iter().filter(|x| **x != 0).next().is_none() { - log::info!("Fix transparent"); - // somehow all 0 images shows black rect, here is a workaround - colors[3] = 1; - } - let mut png = Vec::new(); - if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { - self.call( - "setCursorData", - &make_args!( - cd.id.to_string(), - cd.hotx, - cd.hoty, - cd.width, - cd.height, - &png[..] - ), - ); - } - } - fn get_key_event(&self, down_or_up: i32, name: &str, code: i32) -> Option { let mut key_event = KeyEvent::new(); if down_or_up == 2 { @@ -971,31 +670,6 @@ impl Handler { "".to_owned() } - fn ctrl_alt_del(&mut self) { - if self.peer_platform() == "Windows" { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::CtrlAltDel); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.legacy_modifiers(&mut key_event, true, true, false, false); - // todo - key_event.press = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - } - - fn lock_screen(&mut self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - fn transfer_file(&mut self) { let id = self.get_id(); let args = vec!["--file-transfer", &id, &self.password]; @@ -1011,1756 +685,6 @@ impl Handler { log::error!("Failed to spawn IP tunneling: {}", err); } } - - fn send_key_event(&mut self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { - // mode: legacy(0), map(1), translate(2), auto(3) - evt.mode = keyboard_mode.into(); - let mut msg_out = Message::new(); - msg_out.set_key_event(evt); - self.send(Data::Message(msg_out)); - } - - #[allow(dead_code)] - fn convert_numpad_keys(&mut self, key: RdevKey) -> RdevKey { - if get_key_state(enigo::Key::NumLock) { - return key; - } - match key { - RdevKey::Kp0 => RdevKey::Insert, - RdevKey::KpDecimal => RdevKey::Delete, - RdevKey::Kp1 => RdevKey::End, - RdevKey::Kp2 => RdevKey::DownArrow, - RdevKey::Kp3 => RdevKey::PageDown, - RdevKey::Kp4 => RdevKey::LeftArrow, - RdevKey::Kp5 => RdevKey::Clear, - RdevKey::Kp6 => RdevKey::RightArrow, - RdevKey::Kp7 => RdevKey::Home, - RdevKey::Kp8 => RdevKey::UpArrow, - RdevKey::Kp9 => RdevKey::PageUp, - _ => key, - } - } - - fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, _evt: Option) { - // map mode(1): Send keycode according to the peer platform. - #[cfg(target_os = "windows")] - let key = if let Some(e) = _evt { - rdev::get_win_key(e.code.into(), e.scan_code) - } else { - key - }; - - let peer = self.peer_platform(); - - let mut key_event = KeyEvent::new(); - // According to peer platform. - let keycode: u32 = if peer == "Linux" { - rdev::linux_keycode_from_key(key).unwrap_or_default().into() - } else if peer == "Windows" { - #[cfg(not(windows))] - let key = self.convert_numpad_keys(key); - rdev::win_keycode_from_key(key).unwrap_or_default().into() - } else { - // Without Clear Key on Mac OS - if key == rdev::Key::Clear{ - return; - } - rdev::macos_keycode_from_key(key).unwrap_or_default().into() - }; - - key_event.set_chr(keycode); - key_event.down = down_or_up; - - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - - self.send_key_event(key_event, KeyboardMode::Map); - } - - fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { - // translate mode(2): locally generated characters are send to the peer. - - // get char - let string = match KEYBOARD.lock() { - Ok(mut keyboard) => { - let string = keyboard.add(&evt.event_type).unwrap_or_default(); - if keyboard.is_dead() && string == "" && down_or_up == true { - return; - } - string - } - Err(_) => "".to_owned(), - }; - - // maybe two string - let chars = if string == "" { - None - } else { - let chars: Vec = string.chars().collect(); - Some(chars) - }; - - if let Some(chars) = chars { - for chr in chars { - let mut key_event = KeyEvent::new(); - key_event.set_chr(chr as _); - key_event.down = true; - key_event.press = false; - - self.send_key_event(key_event, KeyboardMode::Translate); - } - } else { - let success = if down_or_up == true { - TO_RELEASE.lock().unwrap().insert(key) - } else { - TO_RELEASE.lock().unwrap().remove(&key) - }; - - // AltGr && LeftControl(SpecialKey) without action - if key == RdevKey::AltGr || evt.scan_code == 541 { - return; - } - if success{ - self.map_keyboard_mode(down_or_up, key, None); - } - } - } - - fn legacy_modifiers( - &self, - key_event: &mut KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - } - - fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { - // legacy mode(0): Generate characters locally, look for keycode on other side. - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - unsafe { - if IS_ALT_GR { - if alt || key == RdevKey::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - RdevKey::Alt => Some(ControlKey::Alt), - RdevKey::AltGr => Some(ControlKey::RAlt), - RdevKey::Backspace => Some(ControlKey::Backspace), - RdevKey::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - RdevKey::ControlRight => Some(ControlKey::RControl), - RdevKey::DownArrow => Some(ControlKey::DownArrow), - RdevKey::Escape => Some(ControlKey::Escape), - RdevKey::F1 => Some(ControlKey::F1), - RdevKey::F10 => Some(ControlKey::F10), - RdevKey::F11 => Some(ControlKey::F11), - RdevKey::F12 => Some(ControlKey::F12), - RdevKey::F2 => Some(ControlKey::F2), - RdevKey::F3 => Some(ControlKey::F3), - RdevKey::F4 => Some(ControlKey::F4), - RdevKey::F5 => Some(ControlKey::F5), - RdevKey::F6 => Some(ControlKey::F6), - RdevKey::F7 => Some(ControlKey::F7), - RdevKey::F8 => Some(ControlKey::F8), - RdevKey::F9 => Some(ControlKey::F9), - RdevKey::LeftArrow => Some(ControlKey::LeftArrow), - RdevKey::MetaLeft => Some(ControlKey::Meta), - RdevKey::MetaRight => Some(ControlKey::RWin), - RdevKey::Return => Some(ControlKey::Return), - RdevKey::RightArrow => Some(ControlKey::RightArrow), - RdevKey::ShiftLeft => Some(ControlKey::Shift), - RdevKey::ShiftRight => Some(ControlKey::RShift), - RdevKey::Space => Some(ControlKey::Space), - RdevKey::Tab => Some(ControlKey::Tab), - RdevKey::UpArrow => Some(ControlKey::UpArrow), - RdevKey::Delete => { - if is_win && ctrl && alt { - self.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - RdevKey::Apps => Some(ControlKey::Apps), - RdevKey::Cancel => Some(ControlKey::Cancel), - RdevKey::Clear => Some(ControlKey::Clear), - RdevKey::Kana => Some(ControlKey::Kana), - RdevKey::Hangul => Some(ControlKey::Hangul), - RdevKey::Junja => Some(ControlKey::Junja), - RdevKey::Final => Some(ControlKey::Final), - RdevKey::Hanja => Some(ControlKey::Hanja), - RdevKey::Hanji => Some(ControlKey::Hanja), - RdevKey::Convert => Some(ControlKey::Convert), - RdevKey::Print => Some(ControlKey::Print), - RdevKey::Select => Some(ControlKey::Select), - RdevKey::Execute => Some(ControlKey::Execute), - RdevKey::PrintScreen => Some(ControlKey::Snapshot), - RdevKey::Help => Some(ControlKey::Help), - RdevKey::Sleep => Some(ControlKey::Sleep), - RdevKey::Separator => Some(ControlKey::Separator), - RdevKey::KpReturn => Some(ControlKey::NumpadEnter), - RdevKey::Kp0 => Some(ControlKey::Numpad0), - RdevKey::Kp1 => Some(ControlKey::Numpad1), - RdevKey::Kp2 => Some(ControlKey::Numpad2), - RdevKey::Kp3 => Some(ControlKey::Numpad3), - RdevKey::Kp4 => Some(ControlKey::Numpad4), - RdevKey::Kp5 => Some(ControlKey::Numpad5), - RdevKey::Kp6 => Some(ControlKey::Numpad6), - RdevKey::Kp7 => Some(ControlKey::Numpad7), - RdevKey::Kp8 => Some(ControlKey::Numpad8), - RdevKey::Kp9 => Some(ControlKey::Numpad9), - RdevKey::KpDivide => Some(ControlKey::Divide), - RdevKey::KpMultiply => Some(ControlKey::Multiply), - RdevKey::KpDecimal => Some(ControlKey::Decimal), - RdevKey::KpMinus => Some(ControlKey::Subtract), - RdevKey::KpPlus => Some(ControlKey::Add), - RdevKey::CapsLock | RdevKey::NumLock | RdevKey::ScrollLock => { - return; - } - RdevKey::Home => Some(ControlKey::Home), - RdevKey::End => Some(ControlKey::End), - RdevKey::Insert => Some(ControlKey::Insert), - RdevKey::PageUp => Some(ControlKey::PageUp), - RdevKey::PageDown => Some(ControlKey::PageDown), - RdevKey::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - RdevKey::Num1 => '1', - RdevKey::Num2 => '2', - RdevKey::Num3 => '3', - RdevKey::Num4 => '4', - RdevKey::Num5 => '5', - RdevKey::Num6 => '6', - RdevKey::Num7 => '7', - RdevKey::Num8 => '8', - RdevKey::Num9 => '9', - RdevKey::Num0 => '0', - RdevKey::KeyA => 'a', - RdevKey::KeyB => 'b', - RdevKey::KeyC => 'c', - RdevKey::KeyD => 'd', - RdevKey::KeyE => 'e', - RdevKey::KeyF => 'f', - RdevKey::KeyG => 'g', - RdevKey::KeyH => 'h', - RdevKey::KeyI => 'i', - RdevKey::KeyJ => 'j', - RdevKey::KeyK => 'k', - RdevKey::KeyL => 'l', - RdevKey::KeyM => 'm', - RdevKey::KeyN => 'n', - RdevKey::KeyO => 'o', - RdevKey::KeyP => 'p', - RdevKey::KeyQ => 'q', - RdevKey::KeyR => 'r', - RdevKey::KeyS => 's', - RdevKey::KeyT => 't', - RdevKey::KeyU => 'u', - RdevKey::KeyV => 'v', - RdevKey::KeyW => 'w', - RdevKey::KeyX => 'x', - RdevKey::KeyY => 'y', - RdevKey::KeyZ => 'z', - RdevKey::Comma => ',', - RdevKey::Dot => '.', - RdevKey::SemiColon => ';', - RdevKey::Quote => '\'', - RdevKey::LeftBracket => '[', - RdevKey::RightBracket => ']', - RdevKey::BackSlash => '\\', - RdevKey::Minus => '-', - RdevKey::Equal => '=', - RdevKey::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - self.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); - - if down_or_up == true { - key_event.down = true; - } - self.send_key_event(key_event, KeyboardMode::Legacy) - } - - fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { - // Call different functions according to keyboard mode. - let mode = match self.get_keyboard_mode().as_str() { - "map" => KeyboardMode::Map, - "legacy" => KeyboardMode::Legacy, - "translate" => KeyboardMode::Translate, - _ => KeyboardMode::Legacy, - }; - - match mode { - KeyboardMode::Map => { - if down_or_up == true { - TO_RELEASE.lock().unwrap().insert(key); - } else { - TO_RELEASE.lock().unwrap().remove(&key); - } - self.map_keyboard_mode(down_or_up, key, Some(evt)); - } - KeyboardMode::Legacy => self.legacy_keyboard_mode(down_or_up, key, evt), - KeyboardMode::Translate => { - self.translate_keyboard_mode(down_or_up, key, evt); - } - _ => self.legacy_keyboard_mode(down_or_up, key, evt), - } - } - - #[inline] - fn set_cursor_id(&mut self, id: String) { - self.call("setCursorId", &make_args!(id)); - } - - #[inline] - fn set_cursor_position(&mut self, cd: CursorPosition) { - self.call("setCursorPosition", &make_args!(cd.x, cd.y)); - } - - #[inline] - fn call(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.element { - allow_err!(e.call_method(func, args)); - } - } - - #[inline] - fn call2(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.element { - allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); - } - } - - #[inline] - fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - self.call("setDisplay", &make_args!(x, y, w, h)); - } -} - -const MILLI1: Duration = Duration::from_millis(1); - -async fn start_one_port_forward( - handler: Handler, - port: i32, - remote_host: String, - remote_port: i32, - receiver: mpsc::UnboundedReceiver, - key: &str, - token: &str, -) { - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); - if let Err(err) = crate::port_forward::listen( - handler.id.clone(), - handler.password.clone(), - port, - handler.clone(), - receiver, - key, - token, - ) - .await - { - handler.on_error(&format!("Failed to listen on {}: {}", port, err)); - } - log::info!("port forward (:{}) exit", port); -} - -#[tokio::main(flavor = "current_thread")] -async fn io_loop(handler: Handler) { - let (sender, mut receiver) = mpsc::unbounded_channel::(); - handler.write().unwrap().sender = Some(sender.clone()); - let mut options = crate::ipc::get_options_async().await; - let mut key = options.remove("key").unwrap_or("".to_owned()); - let token = LocalConfig::get_option("access_token"); - if key.is_empty() { - key = crate::platform::get_license_key(); - } - if handler.is_port_forward() { - if handler.is_rdp() { - let port = handler - .get_option("rdp_port".to_owned()) - .parse::() - .unwrap_or(3389); - std::env::set_var( - "rdp_username", - handler.get_option("rdp_username".to_owned()), - ); - std::env::set_var( - "rdp_password", - handler.get_option("rdp_password".to_owned()), - ); - log::info!("Remote rdp port: {}", port); - start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; - } else if handler.args.len() == 0 { - let pfs = handler.lc.read().unwrap().port_forwards.clone(); - let mut queues = HashMap::>::new(); - for d in pfs { - sender.send(Data::AddPortForward(d)).ok(); - } - loop { - match receiver.recv().await { - Some(Data::AddPortForward((port, remote_host, remote_port))) => { - if port <= 0 || remote_port <= 0 { - continue; - } - let (sender, receiver) = mpsc::unbounded_channel::(); - queues.insert(port, sender); - let handler = handler.clone(); - let key = key.clone(); - let token = token.clone(); - tokio::spawn(async move { - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - }); - } - Some(Data::RemovePortForward(port)) => { - if let Some(s) = queues.remove(&port) { - s.send(Data::Close).ok(); - } - } - Some(Data::Close) => { - break; - } - Some(d) => { - for (_, s) in queues.iter() { - s.send(d.clone()).ok(); - } - } - _ => {} - } - } - } else { - let port = handler.args[0].parse::().unwrap_or(0); - if handler.args.len() != 3 - || handler.args[2].parse::().unwrap_or(0) <= 0 - || port <= 0 - { - handler.on_error("Invalid arguments, usage:

rustdesk --port-forward remote-id listen-port remote-host remote-port"); - } - let remote_host = handler.args[1].clone(); - let remote_port = handler.args[2].parse::().unwrap_or(0); - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - } - return; - } - let frame_count = Arc::new(AtomicUsize::new(0)); - let frame_count_cl = frame_count.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { - frame_count_cl.fetch_add(1, Ordering::Relaxed); - VIDEO - .lock() - .unwrap() - .as_mut() - .map(|v| v.render_frame(data).ok()); - }); - - let mut remote = Remote { - handler, - video_sender, - audio_sender, - receiver, - sender, - old_clipboard: Default::default(), - read_jobs: Vec::new(), - write_jobs: Vec::new(), - remove_jobs: Default::default(), - timer: time::interval(SEC30), - last_update_jobs_status: (Instant::now(), Default::default()), - first_frame: false, - #[cfg(windows)] - clipboard_file_context: None, - data_count: Arc::new(AtomicUsize::new(0)), - frame_count, - video_format: CodecFormat::Unknown, - }; - remote.io_loop(&key, &token).await; - remote.sync_jobs_status_to_local().await; -} - -struct RemoveJob { - files: Vec, - path: String, - sep: &'static str, - is_remote: bool, - no_confirm: bool, - last_update_job_status: Instant, -} - -impl RemoveJob { - fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { - Self { - files, - path, - sep, - is_remote, - no_confirm: false, - last_update_job_status: Instant::now(), - } - } - - pub fn _gen_meta(&self) -> RemoveJobMeta { - RemoveJobMeta { - path: self.path.clone(), - is_remote: self.is_remote, - no_confirm: self.no_confirm, - } - } -} - -struct Remote { - handler: Handler, - video_sender: MediaSender, - audio_sender: MediaSender, - receiver: mpsc::UnboundedReceiver, - sender: mpsc::UnboundedSender, - old_clipboard: Arc>, - read_jobs: Vec, - write_jobs: Vec, - remove_jobs: HashMap, - timer: Interval, - last_update_jobs_status: (Instant, HashMap), - first_frame: bool, - #[cfg(windows)] - clipboard_file_context: Option>, - data_count: Arc, - frame_count: Arc, - video_format: CodecFormat, -} - -impl Remote { - async fn io_loop(&mut self, key: &str, token: &str) { - let stop_clipboard = self.start_clipboard(); - let mut last_recv_time = Instant::now(); - let mut received = false; - let conn_type = if self.handler.is_file_transfer() { - ConnType::FILE_TRANSFER - } else { - ConnType::default() - }; - match Client::start( - &self.handler.id, - key, - token, - conn_type, - self.handler.clone(), - ) - .await - { - Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); - self.handler - .call("setConnectionType", &make_args!(peer.is_secured(), direct)); - - // just build for now - #[cfg(not(windows))] - let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let mut rx_clip_client = get_rx_clip_client().lock().await; - - let mut status_timer = time::interval(Duration::new(1, 0)); - - loop { - tokio::select! { - res = peer.next() => { - if let Some(res) = res { - match res { - Err(err) => { - log::error!("Connection closed: {}", err); - self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - Ok(ref bytes) => { - last_recv_time = Instant::now(); - received = true; - self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); - if !self.handle_msg_from_peer(bytes, &mut peer).await { - break - } - } - } - } else { - if self.handler.is_restarting_remote_device() { - log::info!("Restart remote device"); - self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); - } else { - log::info!("Reset by the peer"); - self.handler.msgbox("error", "Connection Error", "Reset by the peer"); - } - break; - } - } - d = self.receiver.recv() => { - if let Some(d) = d { - if !self.handle_msg_from_ui(d, &mut peer).await { - break; - } - } - } - _msg = rx_clip_client.recv() => { - #[cfg(windows)] - match _msg { - Some((_, clip)) => { - allow_err!(peer.send(&clip_2_msg(clip)).await); - } - None => { - // unreachable!() - } - } - } - _ = self.timer.tick() => { - if last_recv_time.elapsed() >= SEC30 { - self.handler.msgbox("error", "Connection Error", "Timeout"); - break; - } - if !self.read_jobs.is_empty() { - if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - self.update_jobs_status(); - } else { - self.timer = time::interval_at(Instant::now() + SEC30, SEC30); - } - } - _ = status_timer.tick() => { - let speed = self.data_count.swap(0, Ordering::Relaxed); - let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; - self.handler.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), - ..Default::default() - }); - } - } - } - log::debug!("Exit io_loop of id={}", self.handler.id); - } - Err(err) => { - self.handler - .msgbox("error", "Connection Error", &err.to_string()); - } - } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); - } - - fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { - if let Some(job) = self.remove_jobs.get_mut(&id) { - if job.no_confirm { - let file_num = (file_num + 1) as usize; - if file_num < job.files.len() { - let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); - self.sender - .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) - .ok(); - let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; - if elapsed >= 1000 { - job.last_update_job_status = Instant::now(); - } else { - return; - } - } else { - self.remove_jobs.remove(&id); - } - } - } - if let Some(err) = err { - self.handler - .call("jobError", &make_args!(id, err, file_num)); - } else { - self.handler.call("jobDone", &make_args!(id, file_num)); - } - } - - fn start_clipboard(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { - return None; - } - let (tx, rx) = std::sync::mpsc::channel(); - let old_clipboard = self.old_clipboard.clone(); - let tx_protobuf = self.sender.clone(); - let lc = self.handler.lc.clone(); - match ClipboardContext::new() { - Ok(mut ctx) => { - // ignore clipboard update before service start - check_clipboard(&mut ctx, Some(&old_clipboard)); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit clipboard service of client"); - break; - } - _ => {} - } - if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard - { - continue; - } - if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { - tx_protobuf.send(Data::Message(msg)).ok(); - } - }); - } - Err(err) => { - log::error!("Failed to start clipboard service of client: {}", err); - } - } - Some(tx) - } - - async fn load_last_jobs(&mut self) { - log::info!("start load last jobs"); - self.handler.call("clearAllJobs", &make_args!()); - let pc = self.handler.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - // TODO: can add a confirm dialog - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.call( - "addJob", - &make_args!( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false - ), - ); - cnt += 1; - println!("restore read_job: {:?}", job); - } - } - for job_str in pc.transfer.write_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.call( - "addJob", - &make_args!( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true - ), - ); - cnt += 1; - println!("restore write_job: {:?}", job); - } - } - self.handler.call("updateTransferList", &make_args!()); - } - - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { - match data { - Data::Close => { - let mut misc = Misc::new(); - misc.set_close_reason("".to_owned()); - let mut msg = Message::new(); - msg.set_misc(misc); - allow_err!(peer.send(&msg).await); - return false; - } - Data::Login((password, remember)) => { - self.handler - .handle_login_from_ui(password, remember, peer) - .await; - } - Data::ToggleClipboardFile => { - self.check_clipboard_file_context(); - } - Data::Message(msg) => { - allow_err!(peer.send(&msg).await); - } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - log::info!("send files, is remote {}", is_remote); - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!("New job {}, write to {} from remote {}", id, to, path); - self.write_jobs.push(fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - )); - allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) - .await - ); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(job) => { - log::debug!( - "New job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); - #[cfg(not(windows))] - let files = job.files().clone(); - #[cfg(windows)] - let mut files = job.files().clone(); - #[cfg(windows)] - if self.handler.peer_platform() != "Windows" { - // peer is not windows, need transform \ to / - fs::transform_windows_path(&mut files); - } - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); - } - } - } - } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!( - "new write waiting job {}, write to {} from remote {}", - id, - to, - path - ); - let mut job = fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - ); - job.is_last_job = true; - self.write_jobs.push(job); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(mut job) => { - log::debug!( - "new read waiting job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); - job.is_last_job = true; - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - } - } - } - } - Data::ResumeJob((id, is_remote)) => { - if is_remote { - if let Some(job) = get_job(id, &mut self.write_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_send( - id, - job.remote.clone(), - job.file_num, - job.show_hidden - )) - .await - ); - } - } else { - if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone() - )) - .await - ); - } - } - } - Data::SetNoConfirm(id) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - job.no_confirm = true; - } - } - Data::ConfirmDeleteFiles((id, file_num)) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - let i = file_num as usize; - if i < job.files.len() { - self.handler.call( - "confirmDeleteFiles", - &make_args!(id, file_num, job.files[i].name.clone()), - ); - } - } - } - Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { - if is_upload { - if let Some(job) = fs::get_job(id, &mut self.read_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - job.confirm(&FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - } - } else { - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - let mut msg = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - msg.set_file_action(file_action); - allow_err!(peer.send(&msg).await); - } - } - } - Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { - let sep = self.handler.get_path_sep(is_remote); - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_all_files(ReadAllFiles { - id, - path: path.clone(), - include_hidden, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - self.remove_jobs - .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); - } else { - match fs::get_recursive_files(&path, include_hidden) { - Ok(entries) => { - let m = make_fd(id, &entries, true); - self.handler.call("updateFolderFiles", &make_args!(m)); - self.remove_jobs - .insert(id, RemoveJob::new(entries, path, sep, is_remote)); - } - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - } - } - } - Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); - } - fs::remove_job(id, &mut self.read_jobs); - self.remove_jobs.remove(&id); - } - Data::RemoveDir((id, path)) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_dir(FileRemoveDir { - id, - path, - recursive: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } - Data::RemoveFile((id, path, file_num, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_file(FileRemoveFile { - id, - path, - file_num, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::remove_file(&path) { - Err(err) => { - self.handle_job_status(id, file_num, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, file_num, None); - } - } - } - } - Data::CreateDir((id, path, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_create(FileDirCreate { - id, - path, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::create_dir(&path) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, -1, None); - } - } - } - } - _ => {} - } - true - } - - #[inline] - fn update_job_status( - job: &fs::TransferJob, - elapsed: i32, - last_update_jobs_status: &mut (Instant, HashMap), - handler: &mut Handler, - ) { - if elapsed <= 0 { - return; - } - let transferred = job.transferred(); - let last_transferred = { - if let Some(v) = last_update_jobs_status.1.get(&job.id()) { - v.to_owned() - } else { - 0 - } - }; - last_update_jobs_status.1.insert(job.id(), transferred); - let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); - let file_num = job.file_num() - 1; - handler.call( - "jobProgress", - &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - ); - } - - fn update_jobs_status(&mut self) { - let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; - if elapsed >= 1000 { - for job in self.read_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - for job in self.write_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - self.last_update_jobs_status.0 = Instant::now(); - } - } - - async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); - let mut config: PeerConfig = self.handler.load_config(); - let mut transfer_metas = TransferSerde::default(); - for job in self.read_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.read_jobs.push(json_str); - } - for job in self.write_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.write_jobs.push(json_str); - } - log::info!("meta: {:?}", transfer_metas); - config.transfer = transfer_metas; - self.handler.save_config(config); - true - } - - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { - if let Ok(msg_in) = Message::parse_from_bytes(&data) { - match msg_in.union { - Some(message::Union::VideoFrame(vf)) => { - if !self.first_frame { - self.first_frame = true; - self.handler.call2("closeSuccess", &make_args!()); - self.handler.call("adaptSize", &make_args!()); - self.send_opts_after_login(peer).await; - } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); - self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), - ..Default::default() - }) - }; - self.video_sender.send(MediaData::VideoFrame(vf)).ok(); - } - Some(message::Union::Hash(hash)) => { - self.handler - .handle_hash(&self.handler.password.clone(), hash, peer) - .await; - } - Some(message::Union::LoginResponse(lr)) => match lr.union { - Some(login_response::Union::Error(err)) => { - if !self.handler.handle_login_error(&err) { - return false; - } - } - Some(login_response::Union::PeerInfo(pi)) => { - self.handler.handle_peer_info(pi); - self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() - || self.handler.is_port_forward() - || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard) - { - let txt = self.old_clipboard.lock().unwrap().clone(); - if !txt.is_empty() { - let msg_out = crate::create_clipboard_msg(txt); - let sender = self.sender.clone(); - tokio::spawn(async move { - // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - sender.send(Data::Message(msg_out)).ok(); - }); - } - } - - if self.handler.is_file_transfer() { - self.load_last_jobs().await; - } - } - _ => {} - }, - Some(message::Union::CursorData(cd)) => { - self.handler.set_cursor_data(cd); - } - Some(message::Union::CursorId(id)) => { - self.handler.set_cursor_id(id.to_string()); - } - Some(message::Union::CursorPosition(cp)) => { - self.handler.set_cursor_position(cp); - } - Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - update_clipboard(cb, Some(&self.old_clipboard)); - } - } - #[cfg(windows)] - Some(message::Union::Cliprdr(clip)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - if let Some(context) = &mut self.clipboard_file_context { - if let Some(clip) = msg_2_clip(clip) { - server_clip_file(context, 0, clip); - } - } - } - } - Some(message::Union::FileResponse(fr)) => { - match fr.union { - Some(file_response::Union::Dir(fd)) => { - #[cfg(windows)] - let entries = fd.entries.to_vec(); - #[cfg(not(windows))] - let mut entries = fd.entries.to_vec(); - #[cfg(not(windows))] - { - if self.handler.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - } - let mut m = make_fd(fd.id, &entries, fd.id > 0); - if fd.id <= 0 { - m.set_item("path", fd.path); - } - self.handler.call("updateFolderFiles", &make_args!(m)); - if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { - log::info!("job set_files: {:?}", entries); - job.set_files(entries); - } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { - job.files = entries; - } - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - read_path, - true - ), - ); - } - } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }); - allow_err!(peer.send(&msg).await); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - write_path, - false - ), - ); - } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } - }, - Err(err) => { - println!("error recving digest: {}", err); - } - } - } - } - } - } - Some(file_response::Union::Block(block)) => { - log::info!( - "file response block, file id:{}, file num: {}", - block.id, - block.file_num - ); - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job - } - self.update_jobs_status(); - } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); - } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - _ => {} - } - } - Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::AudioFormat(f)) => { - self.audio_sender.send(MediaData::AudioFormat(f)).ok(); - } - Some(misc::Union::ChatMessage(c)) => { - self.handler.call("newMessage", &make_args!(c.text)); - } - Some(misc::Union::PermissionInfo(p)) => { - log::info!("Change permission {:?} -> {}", p.permission, p.enabled); - match p.permission.enum_value_or_default() { - Permission::Keyboard => { - SERVER_KEYBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - self.handler - .call2("setPermission", &make_args!("keyboard", p.enabled)); - } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - self.handler - .call2("setPermission", &make_args!("clipboard", p.enabled)); - } - Permission::Audio => { - self.handler - .call2("setPermission", &make_args!("audio", p.enabled)); - } - Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); - if !p.enabled && self.handler.is_file_transfer() { - return true; - } - self.check_clipboard_file_context(); - self.handler - .call2("setPermission", &make_args!("file", p.enabled)); - } - Permission::Restart => { - self.handler - .call2("setPermission", &make_args!("restart", p.enabled)); - } - } - } - Some(misc::Union::SwitchDisplay(s)) => { - self.handler.call("switchDisplay", &make_args!(s.display)); - self.video_sender.send(MediaData::Reset).ok(); - if s.width > 0 && s.height > 0 { - VIDEO.lock().unwrap().as_mut().map(|v| { - v.stop_streaming().ok(); - let ok = v.start_streaming( - (s.width, s.height), - COLOR_SPACE::Rgb32, - None, - ); - log::info!("[video] reinitialized: {:?}", ok); - }); - self.handler.set_display(s.x, s.y, s.width, s.height); - } - } - Some(misc::Union::CloseReason(c)) => { - self.handler.msgbox("error", "Connection Error", &c); - return false; - } - Some(misc::Union::BackNotification(notification)) => { - if !self.handle_back_notification(notification).await { - return false; - } - } - _ => {} - }, - Some(message::Union::TestDelay(t)) => { - self.handler.handle_test_delay(t, peer).await; - } - Some(message::Union::AudioFrame(frame)) => { - if !self.handler.lc.read().unwrap().disable_audio { - self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); - } - } - Some(message::Union::FileAction(action)) => match action.union { - Some(file_action::Union::SendConfirm(c)) => { - if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); - } - } - _ => {} - }, - _ => {} - } - } - true - } - - async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { - match notification.union { - Some(back_notification::Union::BlockInputState(state)) => { - self.handle_back_msg_block_input( - state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), - ) - .await; - } - Some(back_notification::Union::PrivacyModeState(state)) => { - if !self - .handle_back_msg_privacy_mode( - state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), - ) - .await - { - return false; - } - } - _ => {} - } - true - } - - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - self.handler.call("updateBlockInputState", &make_args!(on)); - } - - async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { - match state { - back_notification::BlockInputState::BlkOnSucceeded => { - self.update_block_input_state(true); - } - back_notification::BlockInputState::BlkOnFailed => { - self.handler - .msgbox("custom-error", "Block user input", "Failed"); - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffSucceeded => { - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffFailed => { - self.handler - .msgbox("custom-error", "Unblock user input", "Failed"); - } - _ => {} - } - } - - #[inline(always)] - fn update_privacy_mode(&mut self, on: bool) { - let mut config = self.handler.load_config(); - config.privacy_mode = on; - self.handler.save_config(config); - - self.handler.call("updatePrivacyMode", &[]); - } - - async fn handle_back_msg_privacy_mode( - &mut self, - state: back_notification::PrivacyModeState, - ) -> bool { - match state { - back_notification::PrivacyModeState::PrvOnByOther => { - self.handler.msgbox( - "error", - "Connecting...", - "Someone turns on privacy mode, exit", - ); - return false; - } - back_notification::PrivacyModeState::PrvNotSupported => { - self.handler - .msgbox("custom-error", "Privacy mode", "Unsupported"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); - self.update_privacy_mode(true); - } - back_notification::PrivacyModeState::PrvOnFailedDenied => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer denied"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailedPlugin => { - self.handler - .msgbox("custom-error", "Privacy mode", "Please install plugins"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffByPeer => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer exit"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); - } - back_notification::PrivacyModeState::PrvOffUnknown => { - self.handler - .msgbox("custom-error", "Privacy mode", "Turned off"); - // log::error!("Privacy mode is turned off with unknown reason"); - self.update_privacy_mode(false); - } - _ => {} - } - true - } - - fn check_clipboard_file_context(&mut self) { - #[cfg(windows)] - { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; - if enabled == self.clipboard_file_context.is_none() { - self.clipboard_file_context = if enabled { - match create_clipboard_file_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - Some(context) - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - None - } - } - } else { - log::info!("clipboard context for file transfer destroyed."); - None - }; - } - } - } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { @@ -2788,148 +712,4 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { } m.set_item("total_size", n as f64); m -} - -#[async_trait] -impl Interface for Handler { - fn send(&self, data: Data) { - if let Some(ref sender) = self.read().unwrap().sender { - sender.send(data).ok(); - } - } - - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { - let retry = check_if_retry(msgtype, title, text); - self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); - } - - fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) - } - - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut pi_sciter = Value::map(); - let username = self.lc.read().unwrap().get_username(&pi); - pi_sciter.set_item("username", username.clone()); - pi_sciter.set_item("hostname", pi.hostname.clone()); - pi_sciter.set_item("platform", pi.platform.clone()); - pi_sciter.set_item("sas_enabled", pi.sas_enabled); - if get_version_number(&pi.version) < get_version_number("1.1.10") { - self.call2("setPermission", &make_args!("restart", false)); - } - if self.is_file_transfer() { - if pi.username.is_empty() { - self.on_error("No active console user logged on, please connect and logon first."); - return; - } - } else if !self.is_port_forward() { - if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(username, pi); - self.call("updatePrivacyMode", &[]); - self.msgbox("error", "Remote Error", "No Display"); - return; - } - let mut displays = Value::array(0); - for ref d in pi.displays.iter() { - let mut display = Value::map(); - display.set_item("x", d.x); - display.set_item("y", d.y); - display.set_item("width", d.width); - display.set_item("height", d.height); - displays.push(display); - } - pi_sciter.set_item("displays", displays); - let mut current = pi.current_display as usize; - if current >= pi.displays.len() { - current = 0; - } - pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current]; - self.set_display(current.x, current.y, current.width, current.height); - // https://sciter.com/forums/topic/color_spaceiyuv-crash - // Nothing spectacular in decoder – done on CPU side. - // So if you can do BGRA translation on your side – the better. - // BGRA is used as internal image format so it will not require additional transformations. - VIDEO.lock().unwrap().as_mut().map(|v| { - let ok = v.start_streaming( - (current.width as _, current.height as _), - COLOR_SPACE::Rgb32, - None, - ); - log::info!("[video] initialized: {:?}", ok); - }); - let p = self.lc.read().unwrap().should_auto_login(); - if !p.is_empty() { - input_os_password(p, true, self.clone()); - } - } - self.lc.write().unwrap().handle_peer_info(username, pi); - self.call("updatePrivacyMode", &[]); - self.call("updatePi", &make_args!(pi_sciter)); - if self.is_file_transfer() { - self.call2("closeSuccess", &make_args!()); - } else if !self.is_port_forward() { - self.msgbox("success", "Successful", "Connected, waiting for image..."); - } - #[cfg(windows)] - { - let mut path = std::env::temp_dir(); - path.push(&self.id); - let path = path.with_extension(crate::get_app_name().to_lowercase()); - std::fs::File::create(&path).ok(); - if let Some(path) = path.to_str() { - crate::platform::windows::add_recent_document(&path); - } - } - self.start_keyboard_hook(); - } - - async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), pass, hash, self, peer).await; - } - - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; - } - - async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - if !t.from_client { - self.update_quality_status(QualityStatus { - delay: Some(t.last_delay as _), - target_bitrate: Some(t.target_bitrate as _), - ..Default::default() - }); - handle_test_delay(t, peer).await; - } - } - - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } - - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } -} - -impl Handler { - fn on_error(&self, err: &str) { - self.msgbox("error", "Error", err); - } -} - -#[tokio::main(flavor = "current_thread")] -async fn send_note(url: String, id: String, conn_id: i32, note: String) { - let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); - allow_err!(crate::post_request(url, body.to_string(), "").await); -} +} \ No newline at end of file diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs new file mode 100644 index 000000000..33949a116 --- /dev/null +++ b/src/ui_session_interface.rs @@ -0,0 +1,1305 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::client::get_key_state; +use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, + load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, + QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, +}; +use crate::common; +use crate::{client::Data, client::Interface}; +use async_trait::async_trait; + +use hbb_common::config::{Config, LocalConfig, PeerConfig}; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{self, sync::mpsc}; +use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; + +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; + +/// IS_IN KEYBOARD_HOOKED sciter only +pub static IS_IN: AtomicBool = AtomicBool::new(false); +static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); + static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); +} + +#[derive(Clone, Default)] +pub struct Session { + pub cmd: String, + pub id: String, + pub password: String, + pub args: Vec, + pub lc: Arc>, + pub sender: Arc>>>, + pub thread: Arc>>>, + pub ui_handler: T, +} + +impl Session { + pub fn get_view_style(&self) -> String { + self.lc.read().unwrap().view_style.clone() + } + + pub fn get_image_quality(&self) -> String { + self.lc.read().unwrap().image_quality.clone() + } + /// Get custom image quality. + pub fn get_custom_image_quality(&self) -> Vec { + self.lc.read().unwrap().custom_image_quality.clone() + } + + pub fn get_keyboard_mode(&mut self) -> String { + return std::env::var("KEYBOARD_MODE") + .unwrap_or(String::from("legacy")) + .to_lowercase(); + } + + pub fn save_keyboard_mode(&mut self, value: String) { + std::env::set_var("KEYBOARD_MODE", value); + } + + pub fn save_view_style(&mut self, value: String) { + self.lc.write().unwrap().save_view_style(value); + } + + pub fn toggle_option(&mut self, name: String) { + let msg = self.lc.write().unwrap().toggle_option(name.clone()); + if name == "enable-file-transfer" { + self.send(Data::ToggleClipboardFile); + } + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + pub fn get_toggle_option(&self, name: String) -> bool { + self.lc.read().unwrap().get_toggle_option(&name) + } + + pub fn is_privacy_mode_supported(&self) -> bool { + self.lc.read().unwrap().is_privacy_mode_supported() + } + + pub fn refresh_video(&self) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + + pub fn save_custom_image_quality(&mut self, custom_image_quality: i32) { + let msg = self + .lc + .write() + .unwrap() + .save_custom_image_quality(custom_image_quality); + self.send(Data::Message(msg)); + } + + pub fn save_image_quality(&mut self, value: String) { + let msg = self.lc.write().unwrap().save_image_quality(value); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + pub fn get_remember(&self) -> bool { + self.lc.read().unwrap().remember + } + + pub fn set_write_override( + &mut self, + job_id: i32, + file_num: i32, + is_override: bool, + remember: bool, + is_upload: bool, + ) -> bool { + self.send(Data::SetConfirmOverrideFile(( + job_id, + file_num, + is_override, + remember, + is_upload, + ))); + true + } + + pub fn has_hwcodec(&self) -> bool { + #[cfg(not(feature = "hwcodec"))] + return false; + #[cfg(feature = "hwcodec")] + return true; + } + + pub fn change_prefer_codec(&self) { + let msg = self.lc.write().unwrap().change_prefer_codec(); + self.send(Data::Message(msg)); + } + + pub fn restart_remote_device(&self) { + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); + self.send(Data::Message(msg)); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + pub fn get_audit_server(&self) -> String { + if self.lc.read().unwrap().conn_id <= 0 + || LocalConfig::get_option("access_token").is_empty() + { + return "".to_owned(); + } + crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ) + } + + pub fn send_note(&self, note: String) { + let url = self.get_audit_server(); + let id = self.id.clone(); + let conn_id = self.lc.read().unwrap().conn_id; + std::thread::spawn(move || { + send_note(url, id, conn_id, note); + }); + } + + pub fn is_xfce(&self) -> bool { + crate::platform::is_xfce() + } + + pub fn remove_port_forward(&self, port: i32) { + let mut config = self.load_config(); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != port) + .collect(); + self.save_config(config); + self.send(Data::RemovePortForward(port)); + } + + pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + let mut config = self.load_config(); + if config + .port_forwards + .iter() + .filter(|x| x.0 == port) + .next() + .is_some() + { + return; + } + let pf = (port, remote_host, remote_port); + config.port_forwards.push(pf.clone()); + self.save_config(config); + self.send(Data::AddPortForward(pf)); + } + + pub fn get_id(&self) -> String { + self.id.clone() + } + + pub fn get_option(&self, k: String) -> String { + if k.eq("remote_dir") { + return self.lc.read().unwrap().get_remote_dir(); + } + self.lc.read().unwrap().get_option(&k) + } + + pub fn set_option(&self, k: String, mut v: String) { + let mut lc = self.lc.write().unwrap(); + if k.eq("remote_dir") { + v = lc.get_all_remote_dir(v); + } + lc.set_option(k, v); + } + + #[inline] + pub fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + #[inline] + pub(super) fn save_config(&self, config: PeerConfig) { + self.lc.write().unwrap().save_config(config); + } + + pub fn is_restarting_remote_device(&self) -> bool { + self.lc.read().unwrap().restarting_remote_device + } + + #[inline] + pub fn peer_platform(&self) -> String { + self.lc.read().unwrap().info.platform.clone() + } + + pub fn ctrl_alt_del(&self) { + if self.peer_platform() == "Windows" { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::CtrlAltDel); + // todo + key_event.down = true; + self.send_key_event(key_event, KeyboardMode::Legacy); + } else { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::Delete); + self.legacy_modifiers(&mut key_event, true, true, false, false); + // todo + key_event.press = true; + self.send_key_event(key_event, KeyboardMode::Legacy); + } + } + + fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { + // mode: legacy(0), map(1), translate(2), auto(3) + evt.mode = keyboard_mode.into(); + let mut msg_out = Message::new(); + msg_out.set_key_event(evt); + self.send(Data::Message(msg_out)); + } + + #[allow(dead_code)] + fn convert_numpad_keys(&mut self, key: RdevKey) -> RdevKey { + if get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + RdevKey::Kp0 => RdevKey::Insert, + RdevKey::KpDecimal => RdevKey::Delete, + RdevKey::Kp1 => RdevKey::End, + RdevKey::Kp2 => RdevKey::DownArrow, + RdevKey::Kp3 => RdevKey::PageDown, + RdevKey::Kp4 => RdevKey::LeftArrow, + RdevKey::Kp5 => RdevKey::Clear, + RdevKey::Kp6 => RdevKey::RightArrow, + RdevKey::Kp7 => RdevKey::Home, + RdevKey::Kp8 => RdevKey::UpArrow, + RdevKey::Kp9 => RdevKey::PageUp, + _ => key, + } + } + + fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, _evt: Option) { + // map mode(1): Send keycode according to the peer platform. + #[cfg(target_os = "windows")] + let key = if let Some(e) = _evt { + rdev::get_win_key(e.code.into(), e.scan_code) + } else { + key + }; + + let peer = self.peer_platform(); + + let mut key_event = KeyEvent::new(); + // According to peer platform. + let keycode: u32 = if peer == "Linux" { + rdev::linux_keycode_from_key(key).unwrap_or_default().into() + } else if peer == "Windows" { + #[cfg(not(windows))] + let key = self.convert_numpad_keys(key); + rdev::win_keycode_from_key(key).unwrap_or_default().into() + } else { + // Without Clear Key on Mac OS + if key == rdev::Key::Clear { + return; + } + rdev::macos_keycode_from_key(key).unwrap_or_default().into() + }; + + key_event.set_chr(keycode); + key_event.down = down_or_up; + + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + + self.send_key_event(key_event, KeyboardMode::Map); + } + + fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + // translate mode(2): locally generated characters are send to the peer. + + // get char + let string = match KEYBOARD.lock() { + Ok(mut keyboard) => { + let string = keyboard.add(&evt.event_type).unwrap_or_default(); + if keyboard.is_dead() && string == "" && down_or_up == true { + return; + } + string + } + Err(_) => "".to_owned(), + }; + + // maybe two string + let chars = if string == "" { + None + } else { + let chars: Vec = string.chars().collect(); + Some(chars) + }; + + if let Some(chars) = chars { + for chr in chars { + let mut key_event = KeyEvent::new(); + key_event.set_chr(chr as _); + key_event.down = true; + key_event.press = false; + + self.send_key_event(key_event, KeyboardMode::Translate); + } + } else { + let success = if down_or_up == true { + TO_RELEASE.lock().unwrap().insert(key) + } else { + TO_RELEASE.lock().unwrap().remove(&key) + }; + + // AltGr && LeftControl(SpecialKey) without action + if key == RdevKey::AltGr || evt.scan_code == 541 { + return; + } + if success { + self.map_keyboard_mode(down_or_up, key, None); + } + } + } + + fn legacy_modifiers( + &self, + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if self.peer_platform() != "Mac OS" { + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + } + } + + fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + // legacy mode(0): Generate characters locally, look for keycode on other side. + let peer = self.peer_platform(); + let is_win = peer == "Windows"; + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = + get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + unsafe { + if IS_ALT_GR { + if alt || key == RdevKey::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + RdevKey::Alt => Some(ControlKey::Alt), + RdevKey::AltGr => Some(ControlKey::RAlt), + RdevKey::Backspace => Some(ControlKey::Backspace), + RdevKey::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if evt.scan_code & 0x200 != 0 { + unsafe { + IS_ALT_GR = true; + } + return; + } + Some(ControlKey::Control) + } + RdevKey::ControlRight => Some(ControlKey::RControl), + RdevKey::DownArrow => Some(ControlKey::DownArrow), + RdevKey::Escape => Some(ControlKey::Escape), + RdevKey::F1 => Some(ControlKey::F1), + RdevKey::F10 => Some(ControlKey::F10), + RdevKey::F11 => Some(ControlKey::F11), + RdevKey::F12 => Some(ControlKey::F12), + RdevKey::F2 => Some(ControlKey::F2), + RdevKey::F3 => Some(ControlKey::F3), + RdevKey::F4 => Some(ControlKey::F4), + RdevKey::F5 => Some(ControlKey::F5), + RdevKey::F6 => Some(ControlKey::F6), + RdevKey::F7 => Some(ControlKey::F7), + RdevKey::F8 => Some(ControlKey::F8), + RdevKey::F9 => Some(ControlKey::F9), + RdevKey::LeftArrow => Some(ControlKey::LeftArrow), + RdevKey::MetaLeft => Some(ControlKey::Meta), + RdevKey::MetaRight => Some(ControlKey::RWin), + RdevKey::Return => Some(ControlKey::Return), + RdevKey::RightArrow => Some(ControlKey::RightArrow), + RdevKey::ShiftLeft => Some(ControlKey::Shift), + RdevKey::ShiftRight => Some(ControlKey::RShift), + RdevKey::Space => Some(ControlKey::Space), + RdevKey::Tab => Some(ControlKey::Tab), + RdevKey::UpArrow => Some(ControlKey::UpArrow), + RdevKey::Delete => { + if is_win && ctrl && alt { + self.ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + RdevKey::Apps => Some(ControlKey::Apps), + RdevKey::Cancel => Some(ControlKey::Cancel), + RdevKey::Clear => Some(ControlKey::Clear), + RdevKey::Kana => Some(ControlKey::Kana), + RdevKey::Hangul => Some(ControlKey::Hangul), + RdevKey::Junja => Some(ControlKey::Junja), + RdevKey::Final => Some(ControlKey::Final), + RdevKey::Hanja => Some(ControlKey::Hanja), + RdevKey::Hanji => Some(ControlKey::Hanja), + RdevKey::Convert => Some(ControlKey::Convert), + RdevKey::Print => Some(ControlKey::Print), + RdevKey::Select => Some(ControlKey::Select), + RdevKey::Execute => Some(ControlKey::Execute), + RdevKey::PrintScreen => Some(ControlKey::Snapshot), + RdevKey::Help => Some(ControlKey::Help), + RdevKey::Sleep => Some(ControlKey::Sleep), + RdevKey::Separator => Some(ControlKey::Separator), + RdevKey::KpReturn => Some(ControlKey::NumpadEnter), + RdevKey::Kp0 => Some(ControlKey::Numpad0), + RdevKey::Kp1 => Some(ControlKey::Numpad1), + RdevKey::Kp2 => Some(ControlKey::Numpad2), + RdevKey::Kp3 => Some(ControlKey::Numpad3), + RdevKey::Kp4 => Some(ControlKey::Numpad4), + RdevKey::Kp5 => Some(ControlKey::Numpad5), + RdevKey::Kp6 => Some(ControlKey::Numpad6), + RdevKey::Kp7 => Some(ControlKey::Numpad7), + RdevKey::Kp8 => Some(ControlKey::Numpad8), + RdevKey::Kp9 => Some(ControlKey::Numpad9), + RdevKey::KpDivide => Some(ControlKey::Divide), + RdevKey::KpMultiply => Some(ControlKey::Multiply), + RdevKey::KpDecimal => Some(ControlKey::Decimal), + RdevKey::KpMinus => Some(ControlKey::Subtract), + RdevKey::KpPlus => Some(ControlKey::Add), + RdevKey::CapsLock | RdevKey::NumLock | RdevKey::ScrollLock => { + return; + } + RdevKey::Home => Some(ControlKey::Home), + RdevKey::End => Some(ControlKey::End), + RdevKey::Insert => Some(ControlKey::Insert), + RdevKey::PageUp => Some(ControlKey::PageUp), + RdevKey::PageDown => Some(ControlKey::PageDown), + RdevKey::Pause => Some(ControlKey::Pause), + _ => None, + }; + let mut key_event = KeyEvent::new(); + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match evt.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + RdevKey::Num1 => '1', + RdevKey::Num2 => '2', + RdevKey::Num3 => '3', + RdevKey::Num4 => '4', + RdevKey::Num5 => '5', + RdevKey::Num6 => '6', + RdevKey::Num7 => '7', + RdevKey::Num8 => '8', + RdevKey::Num9 => '9', + RdevKey::Num0 => '0', + RdevKey::KeyA => 'a', + RdevKey::KeyB => 'b', + RdevKey::KeyC => 'c', + RdevKey::KeyD => 'd', + RdevKey::KeyE => 'e', + RdevKey::KeyF => 'f', + RdevKey::KeyG => 'g', + RdevKey::KeyH => 'h', + RdevKey::KeyI => 'i', + RdevKey::KeyJ => 'j', + RdevKey::KeyK => 'k', + RdevKey::KeyL => 'l', + RdevKey::KeyM => 'm', + RdevKey::KeyN => 'n', + RdevKey::KeyO => 'o', + RdevKey::KeyP => 'p', + RdevKey::KeyQ => 'q', + RdevKey::KeyR => 'r', + RdevKey::KeyS => 's', + RdevKey::KeyT => 't', + RdevKey::KeyU => 'u', + RdevKey::KeyV => 'v', + RdevKey::KeyW => 'w', + RdevKey::KeyX => 'x', + RdevKey::KeyY => 'y', + RdevKey::KeyZ => 'z', + RdevKey::Comma => ',', + RdevKey::Dot => '.', + RdevKey::SemiColon => ';', + RdevKey::Quote => '\'', + RdevKey::LeftBracket => '[', + RdevKey::RightBracket => ']', + RdevKey::BackSlash => '\\', + RdevKey::Minus => '-', + RdevKey::Equal => '=', + RdevKey::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + self.lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", evt); + return; + } + } + + self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + + if down_or_up == true { + key_event.down = true; + } + self.send_key_event(key_event, KeyboardMode::Legacy) + } + + fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + // Call different functions according to keyboard mode. + let mode = match self.get_keyboard_mode().as_str() { + "map" => KeyboardMode::Map, + "legacy" => KeyboardMode::Legacy, + "translate" => KeyboardMode::Translate, + _ => KeyboardMode::Legacy, + }; + + match mode { + KeyboardMode::Map => { + if down_or_up == true { + TO_RELEASE.lock().unwrap().insert(key); + } else { + TO_RELEASE.lock().unwrap().remove(&key); + } + self.map_keyboard_mode(down_or_up, key, Some(evt)); + } + KeyboardMode::Legacy => self.legacy_keyboard_mode(down_or_up, key, evt), + KeyboardMode::Translate => { + self.translate_keyboard_mode(down_or_up, key, evt); + } + _ => self.legacy_keyboard_mode(down_or_up, key, evt), + } + } + + pub fn get_platform(&self, is_remote: bool) -> String { + if is_remote { + self.peer_platform() + } else { + whoami::platform().to_string() + } + } + + pub fn get_path_sep(&self, is_remote: bool) -> &'static str { + let p = self.get_platform(is_remote); + if &p == "Windows" { + return "\\"; + } else { + return "/"; + } + } + + pub fn input_os_password(&self, pass: String, activate: bool) { + input_os_password(pass, activate, self.clone()); + } + + pub fn get_chatbox(&self) -> String { + #[cfg(feature = "inline")] + return super::inline::get_chatbox(); + #[cfg(not(feature = "inline"))] + return "".to_owned(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn get_icon(&self) -> String { + crate::get_icon() + } + + pub fn send_chat(&self, text: String) { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + pub fn switch_display(&self, display: i32) { + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + pub fn lock_screen(&self) { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + // todo + key_event.down = true; + self.send_key_event(key_event, KeyboardMode::Legacy); + } + + // flutter only TODO new input + pub fn input_key( + &self, + name: &str, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let chars: Vec = name.chars().collect(); + if chars.len() == 1 { + let key = Key::_Raw(chars[0] as _); + self._input_key(key, down, press, alt, ctrl, shift, command); + } else { + if let Some(key) = KEY_MAP.get(name) { + self._input_key(key.clone(), down, press, alt, ctrl, shift, command); + } + } + } + + // flutter only TODO new input + pub fn input_string(&self, value: &str) { + let mut key_event = KeyEvent::new(); + key_event.set_seq(value.to_owned()); + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + self.send(Data::Message(msg_out)); + } + + // flutter only TODO new input + fn _input_key( + &self, + key: Key, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let v = if press { + 3 + } else if down { + 1 + } else { + 0 + }; + let mut key_event = KeyEvent::new(); + match key { + Key::Chr(chr) => { + key_event.set_chr(chr); + } + Key::ControlKey(key) => { + key_event.set_control_key(key.clone()); + } + Key::_Raw(raw) => { + if raw > 'z' as u32 || raw < 'a' as u32 { + key_event.set_unicode(raw); + // TODO + // if down_or_up == 0 { + // // ignore up, avoiding trigger twice + // return; + // } + // down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side + } else { + // to make ctrl+c works on windows + key_event.set_chr(raw); + } + } + } + // asur4s-todo + // self.key_down_or_up(v, key_event, alt, ctrl, shift, command); + } + + pub fn send_mouse( + &self, + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + #[allow(unused_mut)] + let mut command = command; + #[cfg(windows)] + { + if !command && crate::platform::windows::get_win_key_state() { + command = true; + } + } + + send_mouse(mask, x, y, alt, ctrl, shift, command, self); + // on macos, ctrl + left button down = right button down, up won't emit, so we need to + // emit up myself if peer is not macos + // to-do: how about ctrl + left from win to macos + if cfg!(target_os = "macos") { + let buttons = mask >> 3; + let evt_type = mask & 0x7; + if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { + self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); + } + } + } + + pub fn reconnect(&self) { + self.send(Data::Close); + let cloned = self.clone(); + let mut lock = self.thread.lock().unwrap(); + lock.take().map(|t| t.join()); + *lock = Some(std::thread::spawn(move || { + io_loop(cloned); + })); + } + + pub fn get_icon_path(&self, file_type: i32, ext: String) -> String { + let mut path = Config::icon_path(); + if file_type == FileType::DirLink as i32 { + let new_path = path.join("dir_link"); + if !std::fs::metadata(&new_path).is_ok() { + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::File as i32 { + if !ext.is_empty() { + path = path.join(format!("file.{}", ext)); + } else { + path = path.join("file"); + } + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + } else if file_type == FileType::FileLink as i32 { + let new_path = path.join("file_link"); + if !std::fs::metadata(&new_path).is_ok() { + path = path.join("file"); + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::DirDrive as i32 { + if cfg!(windows) { + path = fs::get_path("C:"); + } else if cfg!(target_os = "macos") { + if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { + for entry in entries { + if let Ok(entry) = entry { + path = entry.path(); + break; + } + } + } + } + } + fs::get_string(&path) + } + + pub fn login(&self, password: String, remember: bool) { + self.send(Data::Login((password, remember))); + } + + pub fn new_rdp(&self) { + self.send(Data::NewRDP); + } + + pub fn close(&self) { + self.send(Data::Close); + } +} + +pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { + fn set_cursor_data(&self, cd: CursorData); + fn set_cursor_id(&self, id: String); + fn set_cursor_position(&self, cp: CursorPosition); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32); + fn switch_display(&self, display: &SwitchDisplay); + fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn update_privacy_mode(&self); + fn set_permission(&self, name: &str, value: bool); + fn close_success(&self); + fn update_quality_status(&self, qs: QualityStatus); + fn set_connection_type(&self, is_secured: bool, direct: bool); + fn job_error(&self, id: i32, err: String, file_num: i32); + fn job_done(&self, id: i32, file_num: i32); + fn clear_all_jobs(&self); + fn add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ); + fn new_message(&self, msg: String); + fn update_transfer_list(&self); + // fn update_folder_files(&self); // TODO flutter with file_dir and update_folder_files + fn confirm_delete_files(&self, id: i32, i: i32, name: String); + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); + fn update_block_input_state(&self, on: bool); + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); + fn adapt_size(&self); + fn on_rgba(&self, data: &[u8]); + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool); + #[cfg(any(target_os = "android", target_os = "ios"))] + fn clipboard(&self, content: String); +} + +impl Deref for Session { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.ui_handler + } +} + +impl DerefMut for Session { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ui_handler + } +} + +impl FileManager for Session {} + +#[async_trait] +impl Interface for Session { + fn send(&self, data: Data) { + if let Some(sender) = self.sender.read().unwrap().as_ref() { + sender.send(data).ok(); + } + } + + fn is_file_transfer(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) + } + + fn is_port_forward(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) + } + + fn is_rdp(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + let retry = check_if_retry(msgtype, title, text); + self.ui_handler.msgbox(msgtype, title, text, retry); + } + + fn handle_login_error(&mut self, err: &str) -> bool { + self.lc.write().unwrap().handle_login_error(err, self) + } + + fn handle_peer_info(&mut self, mut pi: PeerInfo) { + pi.username = self.lc.read().unwrap().get_username(&pi); + if pi.current_display as usize >= pi.displays.len() { + pi.current_display = 0; + } + if get_version_number(&pi.version) < get_version_number("1.1.10") { + self.set_permission("restart", false); + } + if self.is_file_transfer() { + if pi.username.is_empty() { + self.on_error("No active console user logged on, please connect and logon first."); + return; + } + } else if !self.is_port_forward() { + if pi.displays.is_empty() { + self.lc.write().unwrap().handle_peer_info(&pi); + self.update_privacy_mode(); + self.msgbox("error", "Remote Error", "No Display"); + return; + } + let p = self.lc.read().unwrap().should_auto_login(); + if !p.is_empty() { + input_os_password(p, true, self.clone()); + } + let current = &pi.displays[pi.current_display as usize]; + self.set_display(current.x, current.y, current.width, current.height); + } + self.update_privacy_mode(); + // Save recent peers, then push event to flutter. So flutter can refresh peer page. + self.lc.write().unwrap().handle_peer_info(&pi); + self.set_peer_info(&pi); + if self.is_file_transfer() { + self.close_success(); + } else if !self.is_port_forward() { + self.msgbox("success", "Successful", "Connected, waiting for image..."); + } + #[cfg(windows)] + { + let mut path = std::env::temp_dir(); + path.push(&self.id); + let path = path.with_extension(crate::get_app_name().to_lowercase()); + std::fs::File::create(&path).ok(); + if let Some(path) = path.to_str() { + crate::platform::windows::add_recent_document(&path); + } + } + // TODO use event callbcak + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.start_keyboard_hook(); + } + + async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), pass, hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + if !t.from_client { + self.update_quality_status(QualityStatus { + delay: Some(t.last_delay as _), + target_bitrate: Some(t.target_bitrate as _), + ..Default::default() + }); + handle_test_delay(t, peer).await; + } + } + + fn set_force_relay(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + lc.force_relay = true; + lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } + + fn is_force_relay(&self) -> bool { + self.lc.read().unwrap().force_relay + } +} + +// TODO use event callbcak +// sciter only +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl Session { + fn start_keyboard_hook(&self) { + if self.is_port_forward() || self.is_file_transfer() { + return; + } + if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { + return; + } + log::info!("keyboard hooked"); + let mut me = self.clone(); + #[cfg(windows)] + crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); + std::thread::spawn(move || { + // This will block. + std::env::set_var("KEYBOARD_ONLY", "y"); + lazy_static::lazy_static! { + static ref MUTEX_SPECIAL_KEYS: Mutex> = { + let mut m = HashMap::new(); + m.insert(RdevKey::ShiftLeft, false); + m.insert(RdevKey::ShiftRight, false); + m.insert(RdevKey::ControlLeft, false); + m.insert(RdevKey::ControlRight, false); + m.insert(RdevKey::Alt, false); + m.insert(RdevKey::AltGr, false); + m.insert(RdevKey::MetaLeft, false); + m.insert(RdevKey::MetaRight, false); + Mutex::new(m) + }; + } + + let func = move |evt: Event| { + if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + { + return; + } + let (_key, down) = match evt.event_type { + KeyPress(k) => { + // keyboard long press + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + if *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&k).unwrap() { + return; + } + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); + } + (k, true) + } + KeyRelease(k) => { + // keyboard long press + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); + } + (k, false) + } + _ => return, + }; + me.key_down_or_up(down, _key, evt); + }; + if let Err(error) = rdev::listen(func) { + log::error!("rdev: {:?}", error); + } + }); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn io_loop(handler: Session) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + *handler.sender.write().unwrap() = Some(sender.clone()); + let mut options = crate::ipc::get_options_async().await; + let mut key = options.remove("key").unwrap_or("".to_owned()); + let token = LocalConfig::get_option("access_token"); + if key.is_empty() { + key = crate::platform::get_license_key(); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if handler.is_port_forward() { + if handler.is_rdp() { + let port = handler + .get_option("rdp_port".to_owned()) + .parse::() + .unwrap_or(3389); + std::env::set_var( + "rdp_username", + handler.get_option("rdp_username".to_owned()), + ); + std::env::set_var( + "rdp_password", + handler.get_option("rdp_password".to_owned()), + ); + log::info!("Remote rdp port: {}", port); + start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; + } else if handler.args.len() == 0 { + let pfs = handler.lc.read().unwrap().port_forwards.clone(); + let mut queues = HashMap::>::new(); + for d in pfs { + sender.send(Data::AddPortForward(d)).ok(); + } + loop { + match receiver.recv().await { + Some(Data::AddPortForward((port, remote_host, remote_port))) => { + if port <= 0 || remote_port <= 0 { + continue; + } + let (sender, receiver) = mpsc::unbounded_channel::(); + queues.insert(port, sender); + let handler = handler.clone(); + let key = key.clone(); + let token = token.clone(); + tokio::spawn(async move { + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + }); + } + Some(Data::RemovePortForward(port)) => { + if let Some(s) = queues.remove(&port) { + s.send(Data::Close).ok(); + } + } + Some(Data::Close) => { + break; + } + Some(d) => { + for (_, s) in queues.iter() { + s.send(d.clone()).ok(); + } + } + _ => {} + } + } + } else { + let port = handler.args[0].parse::().unwrap_or(0); + if handler.args.len() != 3 + || handler.args[2].parse::().unwrap_or(0) <= 0 + || port <= 0 + { + handler.on_error("Invalid arguments, usage:

rustdesk --port-forward remote-id listen-port remote-host remote-port"); + } + let remote_host = handler.args[1].clone(); + let remote_port = handler.args[2].parse::().unwrap_or(0); + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + } + return; + } + let frame_count = Arc::new(AtomicUsize::new(0)); + let frame_count_cl = frame_count.clone(); + let ui_handler = handler.ui_handler.clone(); + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { + frame_count_cl.fetch_add(1, Ordering::Relaxed); + ui_handler.on_rgba(data); + }); + + let mut remote = Remote::new( + handler, + video_sender, + audio_sender, + receiver, + sender, + frame_count, + ); + remote.io_loop(&key, &token).await; + remote.sync_jobs_status_to_local().await; +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +async fn start_one_port_forward( + handler: Session, + port: i32, + remote_host: String, + remote_port: i32, + receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, +) { + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + key, + token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await + { + handler.on_error(&format!("Failed to listen on {}: {}", port, err)); + } + log::info!("port forward (:{}) exit", port); +} + +#[tokio::main(flavor = "current_thread")] +async fn send_note(url: String, id: String, conn_id: i32, note: String) { + let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); + allow_err!(crate::post_request(url, body.to_string(), "").await); +}