From e9085ecc440240a7884e6404526dd3078e8f69f0 Mon Sep 17 00:00:00 2001 From: maninhill <41712985+maninhill@users.noreply.github.com> Date: Sat, 27 Aug 2022 19:31:07 +0800 Subject: [PATCH 01/35] chore: spelling correction --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 456862af5..792357225 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 From 98387e06e1243b8104230fd52e3d0784dee0bc37 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 28 Aug 2022 14:58:46 +0800 Subject: [PATCH 02/35] fix: linux main/sub window resize issue Signed-off-by: Kingtous --- flutter/pubspec.lock | 10 +++++----- flutter/pubspec.yaml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 07862bf38..ea5ff449c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: e013c81d75320bbf28adddeaadf462264ee6039d - resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d + ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" + resolved-ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1244,11 +1244,11 @@ packages: dependency: "direct main" description: path: "." - ref: "799ef079e87938c3f4340591b4330c2598f38bb9" - resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + ref: "247818257b4b37f78bebea1719cee765282b3079" + resolved-ref: "247818257b4b37f78bebea1719cee765282b3079" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git - version: "0.2.6" + version: "0.2.7" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 93c2f64b2..b765a5b17 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,11 +61,11 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 799ef079e87938c3f4340591b4330c2598f38bb9 + ref: 247818257b4b37f78bebea1719cee765282b3079 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e013c81d75320bbf28adddeaadf462264ee6039d + ref: 14a001e83ab0e7c8cb119f7f65be4e3056a954fb freezed_annotation: ^2.0.3 tray_manager: git: From 6ea16e4cdb85b958dd0acad158f6f7bebb721fc9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 26 Aug 2022 11:35:28 +0800 Subject: [PATCH 03/35] port forward ui Signed-off-by: 21pages --- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/port_forward_page.dart | 348 ++++++++++++++++++ .../desktop/pages/port_forward_tab_page.dart | 106 ++++++ .../screen/desktop_port_forward_screen.dart | 26 ++ .../lib/desktop/widgets/peercard_widget.dart | 183 ++++++++- flutter/lib/main.dart | 21 ++ flutter/lib/models/model.dart | 17 +- flutter/lib/utils/multi_window_manager.dart | 33 +- src/flutter.rs | 29 +- src/flutter_ffi.rs | 34 +- 10 files changed, 769 insertions(+), 29 deletions(-) create mode 100644 flutter/lib/desktop/pages/port_forward_page.dart create mode 100644 flutter/lib/desktop/pages/port_forward_tab_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_port_forward_screen.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 000a1cb54..3f0abd43f 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,6 +4,7 @@ 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"; 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..b83761181 --- /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.mainAddPortForward( + id: 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.mainRemovePortForward( + id: 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..28825b75a --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -0,0 +1,106 @@ +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/models/model.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()); + + static final IconData selectedIcon = Icons.forward_sharp; + static final IconData unselectedIcon = Icons.forward_outlined; + + _PortForwardTabPageState(Map params) { + tabController.add(TabInfo( + key: params['id'] + params['isRDP'].toString(), + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage( + key: ValueKey(params['id']), + id: params['id'], + isRDP: params['isRDP'], + ))); + } + + @override + void initState() { + super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "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.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(); + } + }); + } + + @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, + isMainWindow: false, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + )), + ), + ); + } + + void onRemoveId(String id) { + ffi("pf_$id").close(); + if (tabController.state.value.tabs.length == 0) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} 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/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 433ca9284..810b84a63 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'; @@ -284,11 +285,20 @@ class _PeerCardState extends State<_PeerCard> /// 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(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); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.new_port_forward(id, isRDP); } else { await rustDeskWinManager.new_remote_desktop(id); } @@ -307,12 +317,18 @@ class _PeerCardState extends State<_PeerCard> items: await super.widget.popupMenuItemsFunc(), elevation: 8, ); - if (value == 'remove') { + if (value == 'connect') { + _connect(id); + } else if (value == 'file') { + _connect(id, isFileTransfer: true); + } else if (value == 'tcp-tunnel') { + _connect(id, isTcpTunneling: true); + } else if (value == 'RDP') { + _connect(id, isRDP: true); + } else 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) { @@ -325,8 +341,6 @@ class _PeerCardState extends State<_PeerCard> 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(); @@ -554,7 +568,7 @@ class RecentPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.recent); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -570,6 +584,10 @@ class RecentPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Add to Favorites')), value: 'add-fav'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -578,7 +596,7 @@ class FavoritePeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.fav); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -594,6 +612,10 @@ class FavoritePeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Remove from Favorites')), value: 'remove-fav'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -602,7 +624,7 @@ class DiscoveredPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.discovered); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -618,6 +640,10 @@ class DiscoveredPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Add to Favorites')), value: 'add-fav'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -626,7 +652,7 @@ class AddressBookPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.ab); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -645,6 +671,10 @@ class AddressBookPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -664,3 +694,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/main.dart b/flutter/lib/main.dart index 9682f19d1..6e30a15d2 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; } @@ -133,6 +137,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)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dda22a779..f9da557f2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1034,17 +1034,24 @@ 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); + final stream = bind.sessionConnect( + id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 9b26870c0..b01b84a9d 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -35,6 +35,7 @@ class RustDeskMultiWindowManager { int? _remoteDesktopWindowId; int? _fileTransferWindowId; + int? _portForwardWindowId; Future new_remote_desktop(String remote_id) async { final msg = @@ -87,6 +88,34 @@ class RustDeskMultiWindowManager { } } + Future new_port_forward(String remote_id, bool isRDP) async { + final msg = jsonEncode({ + "type": WindowType.PortForward.index, + "id": remote_id, + "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 +133,7 @@ class RustDeskMultiWindowManager { case WindowType.FileTransfer: return _fileTransferWindowId; case WindowType.PortForward: - break; + return _portForwardWindowId; case WindowType.Unknown: break; } @@ -120,7 +149,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/src/flutter.rs b/src/flutter.rs index ca00807f1..1a0499565 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -70,7 +70,13 @@ impl Session { /// /// * `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) { + /// * `is_port_forward` - If the session is used for port forward. + pub fn start( + identifier: &str, + is_file_transfer: bool, + is_port_forward: bool, + events2ui: StreamSink, + ) { // TODO check same id let session_id = get_session_id(identifier.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -83,17 +89,17 @@ impl Session { lc: Default::default(), events2ui, }; - session - .lc - .write() - .unwrap() - .initialize(session_id.clone(), is_file_transfer, false); + session.lc.write().unwrap().initialize( + session_id.clone(), + is_file_transfer, + is_port_forward, + ); SESSIONS .write() .unwrap() .insert(identifier.to_owned(), session.clone()); std::thread::spawn(move || { - Connection::start(session, is_file_transfer); + Connection::start(session, is_file_transfer, is_port_forward); }); } @@ -201,7 +207,7 @@ impl Session { self.send(Data::Close); let session = self.clone(); std::thread::spawn(move || { - Connection::start(session, false); + Connection::start(session, false, false); }); } @@ -719,18 +725,21 @@ impl Connection { /// /// * `session` - The session to create a new connection for. /// * `is_file_transfer` - Whether the connection is for file transfer. + /// * `is_port_forward` - Whether the connection is for port forward. #[tokio::main(flavor = "current_thread")] - async fn start(session: Session, is_file_transfer: bool) { + async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { let mut last_recv_time = Instant::now(); let (sender, mut receiver) = mpsc::unbounded_channel::(); let mut stop_clipboard = None; - if !is_file_transfer { + if !is_file_transfer && !is_port_forward { 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 if is_port_forward { + ConnType::PORT_FORWARD // TODO: RDP } else { ConnType::DEFAULT_CONN }; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index aa46e4faf..d9bc31d96 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -111,8 +111,9 @@ pub fn session_connect( events2ui: StreamSink, id: String, is_file_transfer: bool, + is_port_forward: bool, ) -> ResultType<()> { - Session::start(&id, is_file_transfer, events2ui); + Session::start(&id, is_file_transfer, is_port_forward, events2ui); Ok(()) } @@ -592,12 +593,41 @@ 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 main_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { + let mut config = get_peer(id.clone()); + if config + .port_forwards + .iter() + .filter(|x| x.0 == local_port) + .next() + .is_some() + { + return; + } + let pf = (local_port, remote_host, remote_port); + config.port_forwards.push(pf); + config.store(&id); +} + +pub fn main_remove_port_forward(id: String, local_port: i32) { + let mut config = get_peer(id.clone()); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != local_port) + .collect(); + config.store(&id); +} + pub fn main_get_last_remote_id() -> String { // if !config::APP_DIR.read().unwrap().is_empty() { // res = LocalConfig::get_remote_id(); From ea77d9284b96db8d994863e46f612c7f469e45eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 26 Aug 2022 23:28:08 +0800 Subject: [PATCH 04/35] flutter_desktop: new remote menu, mid commit Signed-off-by: fufesou --- flutter/lib/common.dart | 41 +- .../desktop/pages/connection_tab_page.dart | 80 +- .../lib/desktop/pages/desktop_tab_page.dart | 44 +- flutter/lib/desktop/pages/remote_page.dart | 48 +- .../desktop/screen/desktop_remote_screen.dart | 3 + .../widgets/material_mod_popup_menu.dart | 1321 +++++++++++++++++ flutter/lib/desktop/widgets/popup_menu.dart | 375 +++++ .../lib/desktop/widgets/remote_menubar.dart | 560 +++++++ flutter/lib/models/model.dart | 29 +- src/lang/cn.rs | 9 + src/lang/cs.rs | 9 + src/lang/da.rs | 9 + src/lang/de.rs | 9 + src/lang/eo.rs | 9 + src/lang/es.rs | 9 + src/lang/fr.rs | 9 + src/lang/hu.rs | 9 + src/lang/id.rs | 9 + src/lang/it.rs | 9 + src/lang/ja.rs | 9 + src/lang/ko.rs | 9 + src/lang/pl.rs | 9 + src/lang/pt_PT | 9 + src/lang/ptbr.rs | 9 + src/lang/ru.rs | 9 + src/lang/sk.rs | 9 + src/lang/template.rs | 9 + src/lang/tr.rs | 9 + src/lang/tw.rs | 9 + src/lang/vn.rs | 9 + 30 files changed, 2606 insertions(+), 84 deletions(-) create mode 100644 flutter/lib/desktop/widgets/material_mod_popup_menu.dart create mode 100644 flutter/lib/desktop/widgets/popup_menu.dart create mode 100644 flutter/lib/desktop/widgets/remote_menubar.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 17e45ba95..6027fb8de 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -447,7 +447,10 @@ void msgBox( 0, wrap(translate('OK'), () { dialogManager.dismissAll(); - closeConnection(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (type.indexOf("custom") < 0) { + closeConnection(); + } })); } if (hasCancel == null) { @@ -740,3 +743,39 @@ Future>? matchPeers(String searchText, List peers) async { } return filteredList; } + +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)); +} diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index be7c76f2a..c8cde79ad 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -22,26 +22,25 @@ 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; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); if (params['id'] != null) { tabController.add(TabInfo( key: params['id'], label: params['id'], 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(params['id']), + id: params['id'], + tabBarHeight: + fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + )))); } } @@ -54,6 +53,8 @@ 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); @@ -64,14 +65,13 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: RemotePage( - key: ValueKey(id), - id: id, - tabBarHeight: _fullscreenID.value.isNotEmpty - ? 0 - : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - ))); + closable: false, + 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}}"); @@ -88,29 +88,31 @@ 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, + isMainWindow: false, + showTabBar: fullscreen.isFalse, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(fullscreen.isTrue); + return pageView; + }, + ))), + ), + )); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 4a2fdb7d2..a7a93d7ad 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(), + isMainWindow: true, + 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/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 025db279f..8ca9c0cfb 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -9,9 +9,11 @@ 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:tuple/tuple.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'; @@ -21,16 +23,14 @@ import '../../models/platform_model.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(); @@ -50,11 +50,15 @@ class _RemotePageState extends State late FFI _ffi; + void _updateTabBarHeight() { + _ffi.canvasModel.tabBarHeight = widget.tabBarHeight; + } + @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,6 +74,9 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); + PrivacyModeState.init(widget.id); + BlockInputState.init(widget.id); + CurrentDisplayState.init(widget.id); } @override @@ -90,6 +97,9 @@ class _RemotePageState extends State // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); + PrivacyModeState.delete(widget.id); + BlockInputState.delete(widget.id); + CurrentDisplayState.delete(widget.id); } void resetTool() { @@ -217,6 +227,7 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); + _updateTabBarHeight(); return WillPopScope( onWillPop: () async { clientClose(_ffi.dialogManager); @@ -289,6 +300,7 @@ class _RemotePageState extends State } Widget? getBottomAppBar(FfiModel ffiModel) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); return MouseRegion( cursor: SystemMouseCursors.basic, child: BottomAppBar( @@ -323,15 +335,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; }, ) ]) + @@ -404,7 +412,7 @@ class _RemotePageState extends State } if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -418,7 +426,7 @@ class _RemotePageState extends State } if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousedown'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -426,7 +434,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); } } @@ -434,7 +442,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); } } @@ -500,6 +508,10 @@ class _RemotePageState extends State )); } paints.add(QualityMonitor(_ffi.qualityMonitorModel)); + paints.add(RemoteMenubar( + id: widget.id, + ffi: _ffi, + )); return Stack( children: paints, ); 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/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart new file mode 100644 index 000000000..acb8f184c --- /dev/null +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -0,0 +1,375 @@ +import 'dart:core'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tuple/tuple.dart'; + +import './material_mod_popup_menu.dart' as modMenu; + +const kInvalidValueStr = "InvalidValueStr"; + +// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu +class PopupMenuChildrenItem extends modMenu.PopupMenuEntry { + const PopupMenuChildrenItem({ + key, + this.height = kMinInteractiveDimension, + this.padding, + this.enable = true, + this.textStyle, + this.onTap, + this.position = modMenu.PopupMenuPosition.overSide, + this.offset = Offset.zero, + required this.itemBuilder, + required this.child, + }) : super(key: key); + + final modMenu.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 modMenu.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 secondMenuHeight; + final Color commonColor; + + const MenuConfig( + {required this.commonColor, + this.secondMenuHeight = kMinInteractiveDimension}); +} + +abstract class MenuEntryBase { + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf); +} + +class MenuEntryDivider extends MenuEntryBase { + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return const modMenu.PopupMenuDivider(); + } +} + +typedef RadioOptionsGetter = List> Function(); +typedef RadioCurOptionGetter = Future Function(); +typedef RadioOptionSetter = Future Function(String); + +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}) { + () async { + _curOption.value = await curOptionGetter(); + }(); + } + + List> get options => optionsGetter(); + RxString get curOption => _curOption; + setOption(String option) async { + await optionSetter(option); + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } + + modMenu.PopupMenuEntry _buildSecondMenu( + BuildContext context, MenuConfig conf, Tuple2 opt) { + return modMenu.PopupMenuItem( + padding: EdgeInsets.zero, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + child: Row( + children: [ + SizedBox( + width: 20.0, + height: 20.0, + child: Obx(() => opt.item2 == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : SizedBox.shrink())), + const SizedBox(width: MenuConfig.midPadding), + Text( + opt.item1, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ) + ], + ), + ), + onPressed: () { + if (opt.item2 != curOption.value) { + setOption(opt.item2); + } + }, + ), + ); + } + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return PopupMenuChildrenItem( + height: conf.secondMenuHeight, + padding: EdgeInsets.zero, + 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}); + + RxBool get curOption; + Future setOption(bool option); + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return modMenu.PopupMenuItem( + padding: EdgeInsets.zero, + child: Obx( + () => SwitchListTile( + value: curOption.value, + onChanged: (v) { + setOption(v); + }, + title: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + child: Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + dense: true, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: EdgeInsets.only(left: 8.0), + ), + ), + ); + } +} + +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}) + : super(text: text) { + () 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}) + : super(text: text); + + @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 + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return PopupMenuChildrenItem( + height: conf.secondMenuHeight, + padding: EdgeInsets.zero, + position: modMenu.PopupMenuPosition.overSide, + itemBuilder: (BuildContext context) => + entries.map((entry) => entry.build(context, conf)).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, + }); + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return modMenu.PopupMenuItem( + padding: EdgeInsets.zero, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + child: childBuilder( + const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + onPressed: () { + proc(); + }, + ), + ); + } +} + +class CustomMenu { + final List> entries; + final MenuConfig conf; + + const CustomMenu({required this.entries, required this.conf}); + + List> build(BuildContext context) { + return entries.map((entry) => entry.build(context, conf)).toList(); + } +} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart new file mode 100644 index 000000000..9568d0404 --- /dev/null +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -0,0 +1,560 @@ +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:tuple/tuple.dart'; + +import '../../common.dart'; +import '../../mobile/widgets/dialog.dart'; +import '../../mobile/widgets/overlay.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import './popup_menu.dart'; +import './material_mod_popup_menu.dart' as modMenu; + +class _MenubarTheme { + static const Color commonColor = MyTheme.accent; + static const double height = kMinInteractiveDimension; +} + +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 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: 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: 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 + ? Icon( + Icons.fullscreen_exit, + color: _MenubarTheme.commonColor, + ) + : 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: Icon( + Icons.message, + color: _MenubarTheme.commonColor, + ), + ); + } + + Widget _buildMonitor(BuildContext context) { + final pi = widget.ffi.ffiModel.pi; + return modMenu.PopupMenuButton( + tooltip: translate('Select Monitor'), + padding: EdgeInsets.zero, + position: modMenu.PopupMenuPosition.under, + icon: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.personal_video, + color: _MenubarTheme.commonColor, + ), + Padding( + padding: EdgeInsets.only(bottom: 3.9), + child: Obx(() { + RxInt display = CurrentDisplayState.find(widget.id); + return Text( + "${display.value + 1}/${pi.displays.length}", + style: TextStyle(color: _MenubarTheme.commonColor, fontSize: 8), + ); + }), + ) + ], + ), + itemBuilder: (BuildContext context) { + final List rowChildren = []; + final double selectorScale = 1.3; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add(Transform.scale( + scale: selectorScale, + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.personal_video, + color: _MenubarTheme.commonColor, + ), + TextButton( + child: Container( + alignment: AlignmentDirectional.center, + constraints: + BoxConstraints(minHeight: _MenubarTheme.height), + child: Padding( + padding: EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: 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 >[ + modMenu.PopupMenuItem( + height: _MenubarTheme.height, + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren), + ) + ]; + }, + ); + } + + Widget _buildControl(BuildContext context) { + return modMenu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.bolt, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Control Actions'), + position: modMenu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => _getControlMenu() + .map((entry) => entry.build( + context, + MenuConfig( + commonColor: _MenubarTheme.commonColor, + secondMenuHeight: _MenubarTheme.height, + ))) + .toList(), + ); + } + + Widget _buildDisplay(BuildContext context) { + return modMenu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.tv, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Display Settings'), + position: modMenu.PopupMenuPosition.under, + onSelected: (String item) {}, + itemBuilder: (BuildContext context) => _getDisplayMenu() + .map((entry) => entry.build( + context, + MenuConfig( + commonColor: _MenubarTheme.commonColor, + secondMenuHeight: _MenubarTheme.height, + ))) + .toList(), + ); + } + + Widget _buildClose(BuildContext context) { + return IconButton( + tooltip: translate('Close'), + onPressed: () { + clientClose(widget.ffi.dialogManager); + }, + icon: 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: () { + Navigator.pop(context); + bind.sessionRefresh(id: widget.id); + }, + )); + } + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('OS Password'), + style: style, + ), + proc: () { + Navigator.pop(context); + showSetOSPassword(widget.id, false, widget.ffi.dialogManager); + }, + )); + + if (!isWebDesktop) { + if (perms['keyboard'] != false && perms['clipboard'] != false) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Paste'), + style: style, + ), + proc: () { + Navigator.pop(context); + () async { + ClipboardData? data = + await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + bind.sessionInputString(id: widget.id, value: data.text ?? ""); + } + }(); + }, + )); + } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Reset canvas'), + style: style, + ), + proc: () { + Navigator.pop(context); + widget.ffi.cursorModel.reset(); + }, + )); + } + + 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: () { + Navigator.pop(context); + bind.sessionCtrlAltDel(id: widget.id); + }, + )); + } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert Lock'), + style: style, + ), + proc: () { + Navigator.pop(context); + bind.sessionLockScreen(id: widget.id); + }, + )); + + 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: () { + Navigator.pop(context); + RxBool blockInput = BlockInputState.find(widget.id); + bind.sessionToggleOption( + id: widget.id, + value: (blockInput.value ? 'un' : '') + 'block-input'); + blockInput.value = !blockInput.value; + }, + )); + } + } + + 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: () { + Navigator.pop(context); + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); + }, + )); + } + + return displayMenu; + } + + List> _getDisplayMenu() { + final displayMenu = [ + MenuEntrySubRadios( + text: translate('Ratio'), + optionsGetter: () => [ + Tuple2(translate('Original'), 'original'), + Tuple2(translate('Shrink'), 'shrink'), + Tuple2(translate('Stretch'), 'stretch'), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'view-style') ?? + ''; + }, + optionSetter: (String v) async { + await bind.sessionPeerOption( + id: widget.id, name: "view-style", value: v); + widget.ffi.canvasModel.updateViewStyle(); + }), + MenuEntrySubRadios( + text: translate('Scroll Style'), + optionsGetter: () => [ + Tuple2(translate('ScrollAuto'), 'scrollauto'), + Tuple2(translate('Scrollbar'), 'scrollbar'), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'scroll-style') ?? + ''; + }, + optionSetter: (String v) async { + await bind.sessionPeerOption( + id: widget.id, name: "scroll-style", value: v); + widget.ffi.canvasModel.updateScrollStyle(); + }), + MenuEntrySubRadios( + text: translate('Image Quality'), + optionsGetter: () => [ + Tuple2(translate('Good image quality'), 'best'), + Tuple2(translate('Balanced'), 'balanced'), + Tuple2( + translate('Optimize reaction time'), 'low'), + ], + curOptionGetter: () async { + String quality = + await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; + if (quality == '') quality = 'balanced'; + return quality; + }, + optionSetter: (String v) async { + await bind.sessionSetImageQuality(id: widget.id, value: v); + }), + MenuEntrySwitch( + text: translate('Show remote cursor'), + getter: () async { + return await 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 await 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/models/model.dart b/flutter/lib/models/model.dart index dda22a779..b0ac2dc7e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -172,9 +172,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); } }; } @@ -231,9 +231,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); @@ -305,6 +305,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) { @@ -343,13 +349,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) { + // + } } } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 730c8be94..4c66acff4 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -302,5 +302,14 @@ 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", "滚屏方式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f174850fc..fbe0287aa 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 60ebeafbb..5b88db08d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 02c73d095..f41d8c313 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3ce5c24f9..b1207ff47 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 2fa92bac8..a018472f7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4efc804e1..73624290b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fee1fc450..15175175f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b6d9dbd0d..4415488f4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c8ad93476..c8cad290f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -301,5 +301,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5c6ba1da7..2cb0ce6c5 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -299,5 +299,14 @@ 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", "スクロール スタイル"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a0292adcb..24e1db7bf 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -299,5 +299,14 @@ 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", "스크롤 스타일"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 81eaddfaf..16ff7c4e7 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -303,5 +303,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT b/src/lang/pt_PT index e6e282575..adac5af15 100644 --- a/src/lang/pt_PT +++ b/src/lang/pt_PT @@ -299,5 +299,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8d1ffbbcf..cd40fb2a5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -302,5 +302,14 @@ 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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ade2c7806..df07e5e8e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -302,5 +302,14 @@ 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", "Стиль прокрутки"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a887cc34a..935544eb2 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e0b64cdfa..e92a032fb 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -302,5 +302,14 @@ 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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 410d918eb..215c14058 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5f0acdd06..39d636592 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -302,5 +302,14 @@ 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", "滾動樣式"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5704bf9ee..2f9dfcafd 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -302,5 +302,14 @@ 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"), ].iter().cloned().collect(); } From 55ba191ad9b003d8aca50b2195fbf222c1515322 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 28 Aug 2022 21:55:16 +0800 Subject: [PATCH 05/35] flutter_desktop: show/hide menubar tooltip Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 26 ++--- .../lib/desktop/widgets/remote_menubar.dart | 96 ++++++++++--------- src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/pl.rs | 2 + src/lang/{pt_PT => pt_PT.rs} | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/vn.rs | 2 + 23 files changed, 105 insertions(+), 59 deletions(-) rename src/lang/{pt_PT => pt_PT.rs} (99%) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8ca9c0cfb..fbba5bafe 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -197,19 +197,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) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9568d0404..011525ba9 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -10,7 +10,7 @@ import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import './popup_menu.dart'; -import './material_mod_popup_menu.dart' as modMenu; +import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; @@ -50,19 +50,22 @@ class _RemoteMenubarState extends State { } Widget _buildShowHide(BuildContext context) { - return 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, - )))); + 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) { @@ -73,7 +76,7 @@ class _RemoteMenubarState extends State { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), color: _MenubarTheme.commonColor, - icon: Icon(Icons.build), + icon: const Icon(Icons.build), onPressed: () { if (mobileActionsOverlayEntry == null) { showMobileActionsOverlay(); @@ -92,7 +95,7 @@ class _RemoteMenubarState extends State { } menubarItems.add(_buildClose(context)); return PopupMenuTheme( - data: PopupMenuThemeData( + data: const PopupMenuThemeData( textStyle: TextStyle(color: _MenubarTheme.commonColor)), child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( @@ -112,11 +115,11 @@ class _RemoteMenubarState extends State { setFullscreen(!isFullscreen); }, icon: Obx(() => isFullscreen - ? Icon( + ? const Icon( Icons.fullscreen_exit, color: _MenubarTheme.commonColor, ) - : Icon( + : const Icon( Icons.fullscreen, color: _MenubarTheme.commonColor, )), @@ -130,7 +133,7 @@ class _RemoteMenubarState extends State { widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); widget.ffi.chatModel.toggleChatOverlay(); }, - icon: Icon( + icon: const Icon( Icons.message, color: _MenubarTheme.commonColor, ), @@ -139,24 +142,25 @@ class _RemoteMenubarState extends State { Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( tooltip: translate('Select Monitor'), padding: EdgeInsets.zero, - position: modMenu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ - Icon( + const Icon( Icons.personal_video, color: _MenubarTheme.commonColor, ), Padding( - padding: EdgeInsets.only(bottom: 3.9), + padding: const EdgeInsets.only(bottom: 3.9), child: Obx(() { RxInt display = CurrentDisplayState.find(widget.id); return Text( "${display.value + 1}/${pi.displays.length}", - style: TextStyle(color: _MenubarTheme.commonColor, fontSize: 8), + style: const TextStyle( + color: _MenubarTheme.commonColor, fontSize: 8), ); }), ) @@ -164,14 +168,14 @@ class _RemoteMenubarState extends State { ), itemBuilder: (BuildContext context) { final List rowChildren = []; - final double selectorScale = 1.3; + const double selectorScale = 1.3; for (int i = 0; i < pi.displays.length; i++) { rowChildren.add(Transform.scale( scale: selectorScale, child: Stack( alignment: Alignment.center, children: [ - Icon( + const Icon( Icons.personal_video, color: _MenubarTheme.commonColor, ), @@ -179,12 +183,13 @@ class _RemoteMenubarState extends State { child: Container( alignment: AlignmentDirectional.center, constraints: - BoxConstraints(minHeight: _MenubarTheme.height), + const BoxConstraints(minHeight: _MenubarTheme.height), child: Padding( - padding: EdgeInsets.only(bottom: 2.5), + padding: const EdgeInsets.only(bottom: 2.5), child: Text( (i + 1).toString(), - style: TextStyle(color: _MenubarTheme.commonColor), + style: + const TextStyle(color: _MenubarTheme.commonColor), ), )), onPressed: () { @@ -200,8 +205,8 @@ class _RemoteMenubarState extends State { ), )); } - return >[ - modMenu.PopupMenuItem( + return >[ + mod_menu.PopupMenuItem( height: _MenubarTheme.height, padding: EdgeInsets.zero, child: Row( @@ -214,18 +219,18 @@ class _RemoteMenubarState extends State { } Widget _buildControl(BuildContext context) { - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( padding: EdgeInsets.zero, - icon: Icon( + icon: const Icon( Icons.bolt, color: _MenubarTheme.commonColor, ), tooltip: translate('Control Actions'), - position: modMenu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.under, itemBuilder: (BuildContext context) => _getControlMenu() .map((entry) => entry.build( context, - MenuConfig( + const MenuConfig( commonColor: _MenubarTheme.commonColor, secondMenuHeight: _MenubarTheme.height, ))) @@ -234,19 +239,19 @@ class _RemoteMenubarState extends State { } Widget _buildDisplay(BuildContext context) { - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( padding: EdgeInsets.zero, - icon: Icon( + icon: const Icon( Icons.tv, color: _MenubarTheme.commonColor, ), tooltip: translate('Display Settings'), - position: modMenu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.under, onSelected: (String item) {}, itemBuilder: (BuildContext context) => _getDisplayMenu() .map((entry) => entry.build( context, - MenuConfig( + const MenuConfig( commonColor: _MenubarTheme.commonColor, secondMenuHeight: _MenubarTheme.height, ))) @@ -260,7 +265,7 @@ class _RemoteMenubarState extends State { onPressed: () { clientClose(widget.ffi.dialogManager); }, - icon: Icon( + icon: const Icon( Icons.close, color: _MenubarTheme.commonColor, ), @@ -332,7 +337,7 @@ class _RemoteMenubarState extends State { if (pi.platform == 'Linux' || pi.sasEnabled) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( - translate('Insert') + ' Ctrl + Alt + Del', + '${translate("Insert")} Ctrl + Alt + Del', style: style, ), proc: () { @@ -357,8 +362,7 @@ class _RemoteMenubarState extends State { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( - (BlockInputState.find(widget.id).value ? 'Unb' : 'B') + - 'lock user input'), + '${BlockInputState.find(widget.id).value ? "Unb" : "B"}lock user input'), style: style, )), proc: () { @@ -366,7 +370,7 @@ class _RemoteMenubarState extends State { RxBool blockInput = BlockInputState.find(widget.id); bind.sessionToggleOption( id: widget.id, - value: (blockInput.value ? 'un' : '') + 'block-input'); + value: '${blockInput.value ? "un" : ""}block-input'); blockInput.value = !blockInput.value; }, )); @@ -447,7 +451,7 @@ class _RemoteMenubarState extends State { MenuEntrySwitch( text: translate('Show remote cursor'), getter: () async { - return await bind.sessionGetToggleOptionSync( + return bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); }, setter: (bool v) async { @@ -457,7 +461,7 @@ class _RemoteMenubarState extends State { MenuEntrySwitch( text: translate('Show quality monitor'), getter: () async { - return await bind.sessionGetToggleOptionSync( + return bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-quality-monitor'); }, setter: (bool v) async { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4c66acff4..3e50396e6 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "比例"), ("Image Quality", "画质"), ("Scroll Style", "滚屏方式"), + ("Show Menubar", "显示菜单栏"), + ("Hide Menubar", "隐藏菜单栏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index fbe0287aa..f94df1ceb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 5b88db08d..c0f7abf91 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Forhold"), ("Image Quality", "Billede kvalitet"), ("Scroll Style", "Rulstil"), + ("Show Menubar", "Vis menulinje"), + ("Hide Menubar", "skjul menulinjen"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f41d8c313..e411a751d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), ("Scroll Style", "Scroll-Stil"), + ("Show Menubar", "Menüleiste anzeigen"), + ("Hide Menubar", "Menüleiste ausblenden"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index b1207ff47..211e6728d 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporcio"), ("Image Quality", "Bilda Kvalito"), ("Scroll Style", "Ruluma Stilo"), + ("Show Menubar", "Montru menubreton"), + ("Hide Menubar", "kaŝi menubreton"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a018472f7..068442bf4 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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ú"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 73624290b..d568f050b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 15175175f..1fe693248 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 4415488f4..d5d6ed920 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Perbandingan"), ("Image Quality", "Kualitas gambar"), ("Scroll Style", "Gaya Gulir"), + ("Show Menubar", "Tampilkan bilah menu"), + ("Hide Menubar", "sembunyikan bilah menu"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c8cad290f..26e7d4073 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -310,5 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2cb0ce6c5..f1331b01d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -308,5 +308,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "比率"), ("Image Quality", "画質"), ("Scroll Style", "スクロール スタイル"), + ("Show Menubar", "メニューバーを表示"), + ("Hide Menubar", "メニューバーを隠す"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 24e1db7bf..7a0d8dbdf 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -308,5 +308,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "비율"), ("Image Quality", "이미지 품질"), ("Scroll Style", "스크롤 스타일"), + ("Show Menubar", "메뉴 표시줄 표시"), + ("Hide Menubar", "메뉴 표시줄 숨기기"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 16ff7c4e7..6f6326121 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -312,5 +312,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Stosunek"), ("Image Quality", "Jakość obrazu"), ("Scroll Style", "Styl przewijania"), + ("Show Menubar", "Pokaż pasek menu"), + ("Hide Menubar", "ukryj pasek menu"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT b/src/lang/pt_PT.rs similarity index 99% rename from src/lang/pt_PT rename to src/lang/pt_PT.rs index adac5af15..2df6c63dc 100644 --- a/src/lang/pt_PT +++ b/src/lang/pt_PT.rs @@ -308,5 +308,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cd40fb2a5..a0981f867 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", ""), ("Image Quality", ""), ("Scroll Style", ""), + ("Show Menubar", ""), + ("Hide Menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index df07e5e8e..eed658dfd 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Соотношение"), ("Image Quality", "Качество изображения"), ("Scroll Style", "Стиль прокрутки"), + ("Show Menubar", "Показать строку меню"), + ("Hide Menubar", "скрыть строку меню"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 935544eb2..b4e61e83f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Pomer"), ("Image Quality", "Kvalita obrazu"), ("Scroll Style", "Štýl posúvania"), + ("Show Menubar", "Zobraziť panel s ponukami"), + ("Hide Menubar", "skryť panel s ponukami"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e92a032fb..2e5c67cd8 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", ""), ("Image Quality", ""), ("Scroll Style", ""), + ("Show Menubar", ""), + ("Hide Menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 215c14058..829659954 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 39d636592..f7d7cbe1d 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "比例"), ("Image Quality", "畫質"), ("Scroll Style", "滾動樣式"), + ("Show Menubar", "顯示菜單欄"), + ("Hide Menubar", "隱藏菜單欄"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 2f9dfcafd..1c77139b3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } From b004f4b9eed8539d6db3aa2c759f527a59c83e2a Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Aug 2022 21:43:18 +0800 Subject: [PATCH 06/35] fix TextField cursor problem Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 16 ++--- .../desktop/pages/desktop_setting_page.dart | 65 +++++++------------ .../lib/desktop/widgets/peercard_widget.dart | 42 ++++-------- 3 files changed, 42 insertions(+), 81 deletions(-) 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/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 810b84a63..4db43398a 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -462,6 +462,7 @@ class _PeerCardState extends State<_PeerCard> void _rename(String id) async { var isInProgress = false; var name = await bind.mainGetPeerOption(id: id, key: 'alias'); + var controller = TextEditingController(text: name); if (widget.type == PeerType.ab) { final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); if (peer == null) { @@ -470,7 +471,6 @@ class _PeerCardState extends State<_PeerCard> name = peer['alias'] ?? ""; } } - final k = GlobalKey(); gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Rename")), @@ -480,22 +480,9 @@ class _PeerCardState extends State<_PeerCard> Container( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Form( - key: k, child: TextFormField( - controller: TextEditingController(text: name), + controller: controller, 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"; - }, ), ), ), @@ -513,22 +500,17 @@ class _PeerCardState extends State<_PeerCard> 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(); - } + name = controller.text; + 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; }); From e0579a9b57b3235874bf15f099c04144c40c6fa8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Aug 2022 22:28:19 +0800 Subject: [PATCH 07/35] add keeping android font scale factor Signed-off-by: 21pages --- flutter/lib/main.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6e30a15d2..401b7febc 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -211,8 +211,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(), ), From a90973621ad9170b64e41cdadbd5b66340920c58 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 29 Aug 2022 13:08:42 +0800 Subject: [PATCH 08/35] rust port-forward --- .../lib/desktop/pages/port_forward_page.dart | 10 +- src/flutter.rs | 158 +++++++++++++++++- src/flutter_ffi.rs | 28 +--- 3 files changed, 167 insertions(+), 29 deletions(-) diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index b83761181..6cfd0cdb2 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -48,7 +48,7 @@ class _PortForwardPageState extends State void initState() { super.initState(); _ffi = FFI(); - // _ffi.connect(widget.id, isPortForward: true); + _ffi.connect(widget.id, isPortForward: true); Get.put(_ffi, tag: 'pf_${widget.id}'); if (!Platform.isLinux) { Wakelock.enable(); @@ -190,8 +190,8 @@ class _PortForwardPageState extends State remotePort != null && (remoteHostController.text.isEmpty || remoteHostController.text.trim().isNotEmpty)) { - await bind.mainAddPortForward( - id: widget.id, + await bind.sessionAddPortForward( + id: 'pf_${widget.id}', localPort: localPort, remoteHost: remoteHostController.text.trim().isEmpty ? 'localhost' @@ -261,8 +261,8 @@ class _PortForwardPageState extends State child: IconButton( icon: const Icon(Icons.close), onPressed: () async { - await bind.mainRemovePortForward( - id: widget.id, localPort: pf.localPort); + await bind.sessionRemovePortForward( + id: 'pf_${widget.id}', localPort: pf.localPort); refreshTunnelConfig(); }, ), diff --git a/src/flutter.rs b/src/flutter.rs index 1a0499565..880177c78 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -539,6 +539,39 @@ impl Session { ], ); } + + pub 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)); + } + + 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)); + } + + + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err); + } } impl FileManager for Session {} @@ -734,7 +767,7 @@ impl Connection { if !is_file_transfer && !is_port_forward { stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); } - *session.sender.write().unwrap() = Some(sender); + *session.sender.write().unwrap() = Some(sender.clone()); let conn_type = if is_file_transfer { session.lc.write().unwrap().is_file_transfer = true; ConnType::FILE_TRANSFER @@ -743,6 +776,99 @@ impl Connection { } else { ConnType::DEFAULT_CONN }; + let key = Config::get_option("key"); + let token = Config::get_option("access_token"); + + // TODO rdp & cli args + let is_rdp = false; + let args: Vec = Vec::new(); + + if is_port_forward { + if 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 args.len() == 0 { + let pfs = session.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 = session.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 latency_controller = LatencyController::new(); let latency_controller_cl = latency_controller.clone(); @@ -759,8 +885,7 @@ impl Connection { 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)) => { @@ -2288,3 +2413,30 @@ 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, +) { + handler.lc.write().unwrap().port_forward = (remote_host, remote_port); + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + String::new(), // TODO + port, + handler.clone(), + receiver, + key, + token, + ) + .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 d9bc31d96..dd147bb77 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -602,30 +602,16 @@ pub fn main_load_lan_peers() { }; } -pub fn main_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { - let mut config = get_peer(id.clone()); - if config - .port_forwards - .iter() - .filter(|x| x.0 == local_port) - .next() - .is_some() - { - return; +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); } - let pf = (local_port, remote_host, remote_port); - config.port_forwards.push(pf); - config.store(&id); } -pub fn main_remove_port_forward(id: String, local_port: i32) { - let mut config = get_peer(id.clone()); - config.port_forwards = config - .port_forwards - .drain(..) - .filter(|x| x.0 != local_port) - .collect(); - config.store(&id); +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 { From 37617fa88893abd9e9b7c25613746af8b2171586 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 29 Aug 2022 18:37:03 +0800 Subject: [PATCH 09/35] fix port forward session id & file session dispose --- flutter/lib/desktop/pages/file_manager_tab_page.dart | 2 +- flutter/lib/desktop/pages/port_forward_tab_page.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 09577128f..da76890d4 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -57,7 +57,7 @@ class _FileManagerTabPageState extends State { } else if (call.method == "onDestroy") { tabController.state.value.tabs.forEach((tab) { print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; + final tag = 'ft_${tab.label}'; ffi(tag).close().then((_) { Get.delete(tag: tag); }); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 28825b75a..6323a0af9 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -26,7 +26,7 @@ class _PortForwardTabPageState extends State { _PortForwardTabPageState(Map params) { tabController.add(TabInfo( - key: params['id'] + params['isRDP'].toString(), + key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, @@ -61,7 +61,7 @@ class _PortForwardTabPageState extends State { } else if (call.method == "onDestroy") { tabController.state.value.tabs.forEach((tab) { print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; + final tag = 'pf_${tab.label}'; ffi(tag).close().then((_) { Get.delete(tag: tag); }); From fcc62febb1dfc6da4081571a4c89ff86c0c893f0 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 29 Aug 2022 19:45:06 +0800 Subject: [PATCH 10/35] update port-forward 1. fix multi remote port override. 2. add connection.rs port-forward failed to close --- src/flutter.rs | 9 ++++----- src/port_forward.rs | 6 ++++++ src/server/connection.rs | 1 + src/ui/remote.rs | 4 +++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 880177c78..7192c0fdf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -568,7 +568,6 @@ impl Session { self.send(Data::AddPortForward(pf)); } - fn on_error(&self, err: &str) { self.msgbox("error", "Error", err); } @@ -886,7 +885,6 @@ impl Connection { video_format: CodecFormat::Unknown, }; - match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { Ok((mut peer, direct)) => { SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); @@ -2414,7 +2412,6 @@ pub fn get_session_id(id: String) -> String { }; } - async fn start_one_port_forward( handler: Session, port: i32, @@ -2424,7 +2421,6 @@ async fn start_one_port_forward( 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(), String::new(), // TODO @@ -2433,10 +2429,13 @@ async fn start_one_port_forward( 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/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 346477851..d4e353a16 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -949,6 +949,7 @@ impl Connection { addr )) .await; + return false; } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ee0bc3f1d..a0245c28a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1253,7 +1253,6 @@ async fn start_one_port_forward( 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(), @@ -1262,6 +1261,9 @@ async fn start_one_port_forward( receiver, key, token, + handler.lc.clone(), + remote_host, + remote_port, ) .await { From 66a2c51ca509dfb81d6ad07d389d08b1a278bedf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 30 Aug 2022 16:45:47 +0800 Subject: [PATCH 11/35] fix: linux memory-safe workaround Signed-off-by: Kingtous --- .../desktop/pages/connection_tab_page.dart | 16 +++------------ .../desktop/pages/file_manager_tab_page.dart | 20 ++++++------------- .../desktop/pages/port_forward_tab_page.dart | 16 ++++----------- .../lib/desktop/widgets/tabbar_widget.dart | 5 +++-- flutter/lib/models/model.dart | 2 +- flutter/lib/utils/multi_window_manager.dart | 2 +- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 8 files changed, 21 insertions(+), 46 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index c8cde79ad..445590037 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -9,8 +9,6 @@ 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; @@ -73,14 +71,7 @@ class _ConnectionTabPageState extends State { 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.state.value.tabs.clear(); } }); } @@ -116,9 +107,8 @@ class _ConnectionTabPageState extends State { } 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(); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index da76890d4..e391afd71 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'; @@ -42,7 +41,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 +54,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 = 'ft_${tab.label}'; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.state.value.tabs.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( @@ -90,9 +83,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_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 6323a0af9..7555d9745 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -5,7 +5,6 @@ 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/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -44,7 +43,7 @@ class _PortForwardTabPageState extends State { tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( + debugPrint( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_port_forward") { @@ -59,14 +58,7 @@ class _PortForwardTabPageState extends State { unselectedIcon: unselectedIcon, page: PortForwardPage(id: id, isRDP: isRDP))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.forEach((tab) { - print("executing onDestroy hook, closing ${tab.label}}"); - final tag = 'pf_${tab.label}'; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.state.value.tabs.clear(); } }); } @@ -95,8 +87,8 @@ class _PortForwardTabPageState extends State { void onRemoveId(String id) { ffi("pf_$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/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 09f1ee4b5..b126ca7e3 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -6,8 +6,8 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.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'; @@ -323,7 +323,8 @@ class WindowActionPanel extends StatelessWidget { if (mainTab) { windowManager.close(); } else { - WindowController.fromWindowId(windowId!).close(); + // only hide for multi window, not close + WindowController.fromWindowId(windowId!).hide(); } }, is_close: true, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 58ea849ce..70e922bce 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1110,7 +1110,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]. diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index b01b84a9d..fb6ce11ed 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -163,7 +163,7 @@ class RustDeskMultiWindowManager { // no such window already return; } - await WindowController.fromWindowId(wId).close(); + await WindowController.fromWindowId(wId).hide(); } on Error { return; } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ea5ff449c..d2bc7b1a8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" - resolved-ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" + ref: e0368a023ba195462acc00d33ab361b499f0e413 + resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b765a5b17..799a2797a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 14a001e83ab0e7c8cb119f7f65be4e3056a954fb + ref: e0368a023ba195462acc00d33ab361b499f0e413 freezed_annotation: ^2.0.3 tray_manager: git: From c72e48bef1316affc61d2ee09d60c209e137cffa Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 30 Aug 2022 20:48:03 +0800 Subject: [PATCH 12/35] fix: close all typed sessions when hide subwindow --- .../desktop/pages/connection_tab_page.dart | 6 ++-- .../desktop/pages/file_manager_tab_page.dart | 10 ++++-- .../desktop/pages/port_forward_tab_page.dart | 5 ++- .../lib/desktop/widgets/tabbar_widget.dart | 34 ++++++++++++++----- flutter/lib/main.dart | 15 ++++---- flutter/lib/models/native_model.dart | 4 +-- flutter/lib/utils/multi_window_manager.dart | 2 +- flutter/pubspec.yaml | 2 +- 8 files changed, 51 insertions(+), 27 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 445590037..1d00cdc8a 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -63,7 +63,6 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - closable: false, page: Obx(() => RemotePage( key: ValueKey(id), id: id, @@ -71,7 +70,7 @@ class _ConnectionTabPageState extends State { fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, )))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.clear(); + tabController.clear(); } }); } @@ -93,6 +92,9 @@ class _ConnectionTabPageState extends State { theme: theme, isMainWindow: false, showTabBar: fullscreen.isFalse, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index e391afd71..e7f08a516 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -19,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'], @@ -36,7 +37,7 @@ class _FileManagerTabPageState extends State { @override void initState() { super.initState(); - + tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { @@ -54,7 +55,7 @@ class _FileManagerTabPageState extends State { unselectedIcon: unselectedIcon, page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.clear(); + tabController.clear(); } }); } @@ -74,6 +75,9 @@ class _FileManagerTabPageState extends State { controller: tabController, theme: theme, isMainWindow: false, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 7555d9745..8db4c7f98 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -58,7 +58,7 @@ class _PortForwardTabPageState extends State { unselectedIcon: unselectedIcon, page: PortForwardPage(id: id, isRDP: isRDP))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.clear(); + tabController.clear(); } }); } @@ -77,6 +77,9 @@ class _PortForwardTabPageState extends State { controller: tabController, theme: theme, isMainWindow: false, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index b126ca7e3..8ef082b49 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -113,6 +114,11 @@ class DesktopTabController { remove(state.value.selected); } } + + void clear() { + state.value.tabs.clear(); + state.refresh(); + } } class DesktopTab extends StatelessWidget { @@ -127,11 +133,12 @@ class DesktopTab extends StatelessWidget { final bool showClose; final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; + final VoidCallback? onClose; final DesktopTabController controller; - late final state = controller.state; + Rx get state => controller.state; - DesktopTab( + const DesktopTab( {required this.controller, required this.isMainWindow, this.theme = const TarBarTheme.light(), @@ -143,7 +150,8 @@ class DesktopTab extends StatelessWidget { this.showMaximize = true, this.showClose = true, this.pageViewBuilder, - this.tail}); + this.tail, + this.onClose}); @override Widget build(BuildContext context) { @@ -185,6 +193,9 @@ class DesktopTab extends StatelessWidget { Expanded( child: Row( children: [ + Offstage( + offstage: !Platform.isMacOS, + child: const SizedBox(width: 78,)), Row(children: [ Offstage( offstage: !showLogo, @@ -229,6 +240,7 @@ class DesktopTab extends StatelessWidget { showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, + onClose: onClose, ) ], ); @@ -242,6 +254,7 @@ class WindowActionPanel extends StatelessWidget { final bool showMinimize; final bool showMaximize; final bool showClose; + final VoidCallback? onClose; const WindowActionPanel( {Key? key, @@ -249,7 +262,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 @@ -324,8 +338,11 @@ class WindowActionPanel extends StatelessWidget { windowManager.close(); } else { // only hide for multi window, not close - WindowController.fromWindowId(windowId!).hide(); + Future.delayed(Duration.zero, () { + WindowController.fromWindowId(windowId!).hide(); + }); } + onClose?.call(); }, is_close: true, )), @@ -337,13 +354,12 @@ 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; + Rx get state => controller.state; - _ListView( - {required this.controller, required this.onTabClose, required this.theme}) - : this.state = controller.state; + const _ListView( + {required this.controller, required this.onTabClose, required this.theme}); @override Widget build(BuildContext context) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 401b7febc..da3b07567 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -80,14 +80,7 @@ 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); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -95,6 +88,12 @@ 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 { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 55f2d0e79..57372cdb9 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -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/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index fb6ce11ed..b01b84a9d 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -163,7 +163,7 @@ class RustDeskMultiWindowManager { // no such window already return; } - await WindowController.fromWindowId(wId).hide(); + await WindowController.fromWindowId(wId).close(); } on Error { return; } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 799a2797a..fc59b8bd3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -34,7 +34,7 @@ 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 From 01e96a1134a81069528cb58cb9e5a2ca3b02aba8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 29 Aug 2022 18:48:12 +0800 Subject: [PATCH 13/35] flutter_desktop: connection type, mid commit Signed-off-by: fufesou --- flutter/lib/common.dart | 36 ----- flutter/lib/common/shared_state.dart | 73 +++++++++ flutter/lib/consts.dart | 2 + .../desktop/pages/connection_tab_page.dart | 46 +++++- flutter/lib/desktop/pages/remote_page.dart | 30 ++-- flutter/lib/desktop/widgets/popup_menu.dart | 74 ++++----- .../lib/desktop/widgets/remote_menubar.dart | 19 ++- .../lib/desktop/widgets/tabbar_widget.dart | 153 ++++++++++++------ flutter/lib/main.dart | 2 +- flutter/lib/models/model.dart | 33 ++-- 10 files changed, 303 insertions(+), 165 deletions(-) create mode 100644 flutter/lib/common/shared_state.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6027fb8de..b991c7a96 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -743,39 +743,3 @@ Future>? matchPeers(String searchText, List peers) async { } return filteredList; } - -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)); -} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart new file mode 100644 index 000000000..8ff4e667e --- /dev/null +++ b/flutter/lib/common/shared_state.dart @@ -0,0 +1,73 @@ +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; + + void setSecure(bool v) { + _secure.value = v ? 'secure' : 'insecure'; + } + + void setDirect(bool v) { + _direct.value = v ? '' : '_relay'; + } + + 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 ConnectionType collectionType = ConnectionType(); + Get.put(collectionType, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static ConnectionType find(String id) => + Get.find(tag: tag(id)); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3f0abd43f..6c67e2ab9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -10,3 +10,5 @@ const String kTabLabelSettingPage = "Settings"; const int kDefaultDisplayWidth = 1280; const int kDefaultDisplayHeight = 720; + +const kInvalidValueStr = "InvalidValueStr"; diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 1d00cdc8a..75471af0e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -3,6 +3,7 @@ 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'; @@ -20,8 +21,8 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController()); - 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); @@ -34,7 +35,7 @@ class _ConnectionTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, page: Obx(() => RemotePage( - key: ValueKey(params['id']), + key: ValueKey(params['id']), id: params['id'], tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, @@ -88,10 +89,10 @@ class _ConnectionTabPageState extends State { child: Scaffold( backgroundColor: MyTheme.color(context).bg, body: Obx(() => DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - showTabBar: fullscreen.isFalse, + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); }, @@ -103,7 +104,36 @@ class _ConnectionTabPageState extends State { .setFullscreen(fullscreen.isTrue); return pageView; }, - ))), + tabBuilder: (key, icon, label, themeConf) { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final iconName = + '${connectionType.secure.value}${connectionType.direct.value}'; + final connectionIcon = Image.asset( + 'assets/$iconName.png', + width: themeConf.iconSize, + height: themeConf.iconSize, + color: theme.selectedtabIconColor, + ); + //.paddingOnly(right: 5); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + connectionIcon, + label, + ], + ); + } + }))), ), )); } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index fbba5bafe..14635e5a1 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -5,11 +5,9 @@ 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:tuple/tuple.dart'; // import 'package:window_manager/window_manager.dart'; @@ -19,6 +17,8 @@ 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; @@ -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(); @@ -54,6 +54,20 @@ class _RemotePageState extends State _ffi.canvasModel.tabBarHeight = widget.tabBarHeight; } + void _initStates(String id) { + PrivacyModeState.init(id); + BlockInputState.init(id); + CurrentDisplayState.init(id); + ConnectionTypeState.init(id); + } + + void _removeStates(String id) { + PrivacyModeState.delete(id); + BlockInputState.delete(id); + CurrentDisplayState.delete(id); + ConnectionTypeState.delete(id); + } + @override void initState() { super.initState(); @@ -74,14 +88,12 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); - PrivacyModeState.init(widget.id); - BlockInputState.init(widget.id); - CurrentDisplayState.init(widget.id); + _initStates(widget.id); } @override void dispose() { - print("REMOTE PAGE dispose ${widget.id}"); + debugPrint("REMOTE PAGE dispose ${widget.id}"); hideMobileActionsOverlay(); _ffi.listenToMouse(false); _mobileFocusNode.dispose(); @@ -97,9 +109,7 @@ class _RemotePageState extends State // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); - PrivacyModeState.delete(widget.id); - BlockInputState.delete(widget.id); - CurrentDisplayState.delete(widget.id); + _removeStates(widget.id); } void resetTool() { diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index acb8f184c..00f940fdb 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -4,12 +4,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tuple/tuple.dart'; -import './material_mod_popup_menu.dart' as modMenu; - -const kInvalidValueStr = "InvalidValueStr"; +import './material_mod_popup_menu.dart' as mod_menu; // https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu -class PopupMenuChildrenItem extends modMenu.PopupMenuEntry { +class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { const PopupMenuChildrenItem({ key, this.height = kMinInteractiveDimension, @@ -17,19 +15,19 @@ class PopupMenuChildrenItem extends modMenu.PopupMenuEntry { this.enable = true, this.textStyle, this.onTap, - this.position = modMenu.PopupMenuPosition.overSide, + this.position = mod_menu.PopupMenuPosition.overSide, this.offset = Offset.zero, required this.itemBuilder, required this.child, }) : super(key: key); - final modMenu.PopupMenuPosition position; + 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 List> Function(BuildContext) itemBuilder; final Widget child; @override @@ -59,7 +57,7 @@ class MyPopupMenuItemState> popupMenuTheme.textStyle ?? theme.textTheme.subtitle1!; - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( enabled: widget.enable, position: widget.position, offset: widget.offset, @@ -88,22 +86,26 @@ class MenuConfig { static const iconWidth = 12.0; static const iconHeight = 12.0; - final double secondMenuHeight; + final double height; + final double dividerHeight; final Color commonColor; const MenuConfig( {required this.commonColor, - this.secondMenuHeight = kMinInteractiveDimension}); + this.height = kMinInteractiveDimension, + this.dividerHeight = 16.0}); } abstract class MenuEntryBase { - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf); + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf); } class MenuEntryDivider extends MenuEntryBase { @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return const modMenu.PopupMenuDivider(); + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return mod_menu.PopupMenuDivider( + height: conf.dividerHeight, + ); } } @@ -138,14 +140,15 @@ class MenuEntrySubRadios extends MenuEntryBase { } } - modMenu.PopupMenuEntry _buildSecondMenu( + mod_menu.PopupMenuEntry _buildSecondMenu( BuildContext context, MenuConfig conf, Tuple2 opt) { - return modMenu.PopupMenuItem( + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, + height: conf.height, child: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + constraints: BoxConstraints(minHeight: conf.height), child: Row( children: [ SizedBox( @@ -156,7 +159,7 @@ class MenuEntrySubRadios extends MenuEntryBase { Icons.check, color: conf.commonColor, ) - : SizedBox.shrink())), + : const SizedBox.shrink())), const SizedBox(width: MenuConfig.midPadding), Text( opt.item1, @@ -178,10 +181,10 @@ class MenuEntrySubRadios extends MenuEntryBase { } @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { return PopupMenuChildrenItem( - height: conf.secondMenuHeight, padding: EdgeInsets.zero, + height: conf.height, itemBuilder: (BuildContext context) => options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), child: Row(children: [ @@ -218,9 +221,10 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { Future setOption(bool option); @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return modMenu.PopupMenuItem( + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, + height: conf.height, child: Obx( () => SwitchListTile( value: curOption.value, @@ -229,7 +233,7 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { }, title: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + constraints: BoxConstraints(minHeight: conf.height), child: Text( text, style: const TextStyle( @@ -242,7 +246,7 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { horizontal: VisualDensity.minimumDensity, vertical: VisualDensity.minimumDensity, ), - contentPadding: EdgeInsets.only(left: 8.0), + contentPadding: const EdgeInsets.only(left: 8.0), ), ), ); @@ -303,11 +307,11 @@ class MenuEntrySubMenu extends MenuEntryBase { }); @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { return PopupMenuChildrenItem( - height: conf.secondMenuHeight, + height: conf.height, padding: EdgeInsets.zero, - position: modMenu.PopupMenuPosition.overSide, + position: mod_menu.PopupMenuPosition.overSide, itemBuilder: (BuildContext context) => entries.map((entry) => entry.build(context, conf)).toList(), child: Row(children: [ @@ -342,13 +346,14 @@ class MenuEntryButton extends MenuEntryBase { }); @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return modMenu.PopupMenuItem( + mod_menu.PopupMenuEntry 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.secondMenuHeight), + constraints: BoxConstraints(minHeight: conf.height), child: childBuilder( const TextStyle( color: Colors.black, @@ -362,14 +367,3 @@ class MenuEntryButton extends MenuEntryBase { ); } } - -class CustomMenu { - final List> entries; - final MenuConfig conf; - - const CustomMenu({required this.entries, required this.conf}); - - List> build(BuildContext context) { - return entries.map((entry) => entry.build(context, conf)).toList(); - } -} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 011525ba9..620f5f226 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -9,12 +9,15 @@ 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; - static const double height = kMinInteractiveDimension; + // kMinInteractiveDimension + static const double height = 24.0; + static const double dividerHeight = 12.0; } class RemoteMenubar extends StatefulWidget { @@ -168,11 +171,9 @@ class _RemoteMenubarState extends State { ), itemBuilder: (BuildContext context) { final List rowChildren = []; - const double selectorScale = 1.3; for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add(Transform.scale( - scale: selectorScale, - child: Stack( + rowChildren.add( + Stack( alignment: Alignment.center, children: [ const Icon( @@ -203,7 +204,7 @@ class _RemoteMenubarState extends State { ) ], ), - )); + ); } return >[ mod_menu.PopupMenuItem( @@ -232,7 +233,8 @@ class _RemoteMenubarState extends State { context, const MenuConfig( commonColor: _MenubarTheme.commonColor, - secondMenuHeight: _MenubarTheme.height, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, ))) .toList(), ); @@ -253,7 +255,8 @@ class _RemoteMenubarState extends State { context, const MenuConfig( commonColor: _MenubarTheme.commonColor, - secondMenuHeight: _MenubarTheme.height, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, ))) .toList(), ); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 8ef082b49..6e0ce747d 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -121,6 +121,16 @@ class DesktopTabController { } } +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; @@ -134,24 +144,29 @@ class DesktopTab extends StatelessWidget { final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; final VoidCallback? onClose; + final TabBuilder? tabBuilder; + final LabelGetter? labelGetter; final DesktopTabController controller; Rx get state => controller.state; - const 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, - this.onClose}); + const 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, + this.onClose, + this.tabBuilder, + this.labelGetter, + }); @override Widget build(BuildContext context) { @@ -194,8 +209,10 @@ class DesktopTab extends StatelessWidget { child: Row( children: [ Offstage( - offstage: !Platform.isMacOS, - child: const SizedBox(width: 78,)), + offstage: !Platform.isMacOS, + child: const SizedBox( + width: 78, + )), Row(children: [ Offstage( offstage: !showLogo, @@ -228,6 +245,8 @@ class DesktopTab extends StatelessWidget { controller: controller, onTabClose: onTabClose, theme: theme, + tabBuilder: tabBuilder, + labelGetter: labelGetter, )), ), ], @@ -356,10 +375,18 @@ class _ListView extends StatelessWidget { final DesktopTabController controller; final Function(String key)? onTabClose; final TarBarTheme theme; + + final TabBuilder? tabBuilder; + final LabelGetter? labelGetter; + Rx get state => controller.state; - const _ListView( - {required this.controller, required this.onTabClose, required this.theme}); + _ListView( + {required this.controller, + required this.onTabClose, + required this.theme, + this.tabBuilder, + this.labelGetter}); @override Widget build(BuildContext context) { @@ -373,7 +400,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, @@ -381,6 +410,16 @@ 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())); } @@ -388,7 +427,7 @@ class _ListView extends StatelessWidget { class _Tab extends StatelessWidget { late final int index; - late final String label; + late final Rx label; late final IconData? selectedIcon; late final IconData? unselectedIcon; late final bool closable; @@ -397,6 +436,8 @@ class _Tab extends StatelessWidget { 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, @@ -404,6 +445,7 @@ class _Tab extends StatelessWidget { required this.label, this.selectedIcon, this.unselectedIcon, + this.tabBuilder, required this.closable, required this.selected, required this.onClose, @@ -411,11 +453,49 @@ class _Tab extends StatelessWidget { required this.theme}) : super(key: key); + Widget _buildTabContent() { + bool showIcon = selectedIcon != null && unselectedIcon != null; + bool isSelected = index == selected; + + final icon = Offstage( + offstage: !showIcon, + child: Icon( + isSelected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: isSelected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5)); + final labelWidget = Obx(() { + return Text( + translate(label.value), + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ); + }); + + if (tabBuilder == null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + labelWidget, + ], + ); + } else { + return tabBuilder!( + icon, labelWidget, TabThemeConf(iconSize: _kIconSize, theme: 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 showIcon = selectedIcon != null && unselectedIcon != null; + bool isSelected = index == selected; + bool showDivider = index != selected - 1 && index != selected; return Ink( child: InkWell( onHover: (hover) => _hover.value = hover, @@ -427,40 +507,19 @@ class _Tab extends StatelessWidget { 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), - ), - ], - ), + _buildTabContent(), Offstage( offstage: !closable, child: Obx((() => _CloseButton( visiable: _hover.value, - tabSelected: is_selected, + tabSelected: isSelected, onClose: () => onClose(), theme: theme, ))), ) ])).paddingSymmetric(horizontal: 10), Offstage( - offstage: !show_divider, + offstage: !showDivider, child: VerticalDivider( width: 1, indent: _kDividerIndent, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index da3b07567..efca88180 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -202,7 +202,7 @@ class App extends StatelessWidget { title: 'RustDesk', theme: getCurrentTheme(), home: isDesktop - ? DesktopTabPage() + ? const DesktopTabPage() : !isAndroid ? WebHomePage() : HomePage(), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 70e922bce..4a54ba4e8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,6 +17,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 +97,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 +132,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') { @@ -189,7 +192,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') { From f42c6ffeaf6ec66101d7389586bab7fe906453aa Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 29 Aug 2022 22:46:19 +0800 Subject: [PATCH 14/35] flutter_desktop: connection type icon, tested windows Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 24 +++-- .../desktop/pages/connection_tab_page.dart | 90 +++++++++++-------- flutter/lib/desktop/pages/remote_page.dart | 2 - src/lang/cn.rs | 4 + src/lang/cs.rs | 4 + src/lang/da.rs | 4 + src/lang/de.rs | 4 + src/lang/eo.rs | 4 + src/lang/es.rs | 4 + src/lang/fr.rs | 4 + src/lang/hu.rs | 4 + src/lang/id.rs | 4 + src/lang/it.rs | 4 + src/lang/ja.rs | 4 + src/lang/ko.rs | 4 + src/lang/pl.rs | 4 + src/lang/pt_PT.rs | 4 + src/lang/ptbr.rs | 4 + src/lang/ru.rs | 4 + src/lang/sk.rs | 4 + src/lang/template.rs | 4 + src/lang/tr.rs | 4 + src/lang/tw.rs | 4 + src/lang/vn.rs | 4 + 24 files changed, 154 insertions(+), 46 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 8ff4e667e..7232cb6ad 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -45,12 +45,17 @@ class ConnectionType { 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 ? 'secure' : 'insecure'; + _secure.value = v ? strSecure : strInsecure; } void setDirect(bool v) { - _direct.value = v ? '' : '_relay'; + _direct.value = v ? strDirect : strIndirect; } bool isValid() { @@ -63,11 +68,20 @@ class ConnectionTypeState { static String tag(String id) => 'connection_type_$id'; static void init(String id) { - final ConnectionType collectionType = ConnectionType(); - Get.put(collectionType, tag: tag(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 void delete(String id) => Get.delete(tag: tag(id)); static ConnectionType find(String id) => Get.find(tag: tag(id)); } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 75471af0e..1b9e04ebf 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -28,15 +28,17 @@ class _ConnectionTabPageState extends State { _ConnectionTabPageState(Map params) { final RxBool fullscreen = Get.find(tag: 'fullscreen'); - if (params['id'] != null) { + 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: Obx(() => RemotePage( - key: ValueKey(params['id']), - id: params['id'], + key: ValueKey(peerId), + id: peerId, tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, )))); @@ -89,10 +91,10 @@ class _ConnectionTabPageState extends State { child: Scaffold( backgroundColor: MyTheme.color(context).bg, body: Obx(() => DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - showTabBar: fullscreen.isFalse, + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); }, @@ -104,36 +106,45 @@ class _ConnectionTabPageState extends State { .setFullscreen(fullscreen.isTrue); return pageView; }, - tabBuilder: (key, icon, label, themeConf) { - final connectionType = ConnectionTypeState.find(key); - if (!connectionType.isValid()) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - label, - ], - ); - } else { - final iconName = - '${connectionType.secure.value}${connectionType.direct.value}'; - final connectionIcon = Image.asset( - 'assets/$iconName.png', - width: themeConf.iconSize, - height: themeConf.iconSize, - color: theme.selectedtabIconColor, - ); - //.paddingOnly(right: 5); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - connectionIcon, - label, - ], - ); - } - }))), + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!ConnectionTypeState.find(key).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, + ], + ); + } + }), + ))), ), )); } @@ -142,6 +153,7 @@ class _ConnectionTabPageState extends State { if (tabController.state.value.tabs.isEmpty) { WindowController.fromWindowId(windowId()).hide(); } + ConnectionTypeState.delete(id); } int windowId() { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 14635e5a1..f723b17d0 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -58,14 +58,12 @@ class _RemotePageState extends State PrivacyModeState.init(id); BlockInputState.init(id); CurrentDisplayState.init(id); - ConnectionTypeState.init(id); } void _removeStates(String id) { PrivacyModeState.delete(id); BlockInputState.delete(id); CurrentDisplayState.delete(id); - ConnectionTypeState.delete(id); } @override diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 3e50396e6..fdb23f88e 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "滚屏方式"), ("Show Menubar", "显示菜单栏"), ("Hide Menubar", "隐藏菜单栏"), + ("Direct Connection", "直接连接"), + ("Relay Connection", "中继连接"), + ("Secure Connection", "安全连接"), + ("Insecure Connection", "非安全连接"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f94df1ceb..d9ddf78cc 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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í"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c0f7abf91..0e2d99425 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e411a751d..20cd9330e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 211e6728d..fe12e2d24 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 068442bf4..313ea8cac 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d568f050b..c8b12243f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 1fe693248..a6356b000 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index d5d6ed920..8548eb6bc 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 26e7d4073..fdf8d27d9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -312,5 +312,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index f1331b01d..1d031f2f2 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "スクロール スタイル"), ("Show Menubar", "メニューバーを表示"), ("Hide Menubar", "メニューバーを隠す"), + ("Direct Connection", "直接接続"), + ("Relay Connection", "リレー接続"), + ("Secure Connection", "安全な接続"), + ("Insecure Connection", "安全でない接続"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7a0d8dbdf..19d4c7ddf 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "스크롤 스타일"), ("Show Menubar", "메뉴 표시줄 표시"), ("Hide Menubar", "메뉴 표시줄 숨기기"), + ("Direct Connection", "직접 연결"), + ("Relay Connection", "릴레이 연결"), + ("Secure Connection", "보안 연결"), + ("Insecure Connection", "안전하지 않은 연결"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 6f6326121..251c349a2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -314,5 +314,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 2df6c63dc..fd4384767 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a0981f867..85eda60e6 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", ""), ("Show Menubar", ""), ("Hide Menubar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index eed658dfd..def560217 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Стиль прокрутки"), ("Show Menubar", "Показать строку меню"), ("Hide Menubar", "скрыть строку меню"), + ("Direct Connection", "Прямая связь"), + ("Relay Connection", "Релейное соединение"), + ("Secure Connection", "Безопасное соединение"), + ("Insecure Connection", "Небезопасное соединение"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index b4e61e83f..4c04618aa 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 2e5c67cd8..081b7bf55 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", ""), ("Show Menubar", ""), ("Hide Menubar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 829659954..9738ed469 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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ı"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f7d7cbe1d..46276dd2a 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "滾動樣式"), ("Show Menubar", "顯示菜單欄"), ("Hide Menubar", "隱藏菜單欄"), + ("Direct Connection", "直接連接"), + ("Relay Connection", "中繼連接"), + ("Secure Connection", "安全連接"), + ("Insecure Connection", "非安全連接"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1c77139b3..474e57337 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ].iter().cloned().collect(); } From 4d914e9a01801d5c1909ffab942197b0a7e68fe8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 30 Aug 2022 17:20:25 +0800 Subject: [PATCH 15/35] flutter_desktop: remote menubar remove submenu Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 321 ++++++++++++------ .../lib/desktop/widgets/remote_menubar.dart | 11 +- flutter/pubspec.yaml | 5 +- 3 files changed, 219 insertions(+), 118 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 00f940fdb..3d5fdf7f6 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -97,15 +97,18 @@ class MenuConfig { } abstract class MenuEntryBase { - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf); + List> build(BuildContext context, MenuConfig conf); } class MenuEntryDivider extends MenuEntryBase { @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return mod_menu.PopupMenuDivider( - height: conf.dividerHeight, - ); + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuDivider( + height: conf.dividerHeight, + ) + ]; } } @@ -113,6 +116,85 @@ typedef RadioOptionsGetter = List> Function(); typedef RadioCurOptionGetter = Future Function(); typedef RadioOptionSetter = Future Function(String); +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}) { + () async { + _curOption.value = await curOptionGetter(); + }(); + } + + List> get options => optionsGetter(); + RxString get curOption => _curOption; + setOption(String option) async { + await optionSetter(option); + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } + + mod_menu.PopupMenuEntry _buildMenuItem( + BuildContext context, MenuConfig conf, Tuple2 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.item1, + 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.item2 == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : const SizedBox.shrink())), + )), + ], + ), + ), + onPressed: () { + if (opt.item2 != curOption.value) { + setOption(opt.item2); + } + }, + ), + ); + } + + @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; @@ -151,23 +233,26 @@ class MenuEntrySubRadios extends MenuEntryBase { constraints: BoxConstraints(minHeight: conf.height), child: Row( children: [ - SizedBox( - width: 20.0, - height: 20.0, - child: Obx(() => opt.item2 == curOption.value - ? Icon( - Icons.check, - color: conf.commonColor, - ) - : const SizedBox.shrink())), - const SizedBox(width: MenuConfig.midPadding), Text( opt.item1, 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.item2 == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : const SizedBox.shrink())), + )), ], ), ), @@ -181,31 +266,34 @@ class MenuEntrySubRadios extends MenuEntryBase { } @override - mod_menu.PopupMenuEntry 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, + 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, + ), + )) + ]), + ) + ]; } } @@ -221,35 +309,38 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { Future setOption(bool option); @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return mod_menu.PopupMenuItem( - padding: EdgeInsets.zero, - height: conf.height, - child: Obx( - () => SwitchListTile( - value: curOption.value, - onChanged: (v) { - setOption(v); - }, - title: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: Text( - text, - style: const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - )), - dense: true, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: Obx( + () => SwitchListTile( + value: curOption.value, + onChanged: (v) { + setOption(v); + }, + title: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + dense: true, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: const EdgeInsets.only(left: 8.0), ), - contentPadding: const EdgeInsets.only(left: 8.0), ), - ), - ); + ) + ]; } } @@ -307,32 +398,37 @@ class MenuEntrySubMenu extends MenuEntryBase { }); @override - mod_menu.PopupMenuEntry 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)).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, + 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, + ), + )) + ]), + ) + ]; } } @@ -346,24 +442,27 @@ class MenuEntryButton extends MenuEntryBase { }); @override - mod_menu.PopupMenuEntry 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: () { - proc(); - }, - ), - ); + 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: () { + proc(); + }, + ), + ) + ]; } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 620f5f226..0e931dd71 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -236,6 +236,7 @@ class _RemoteMenubarState extends State { height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) + .expand((i) => i) .toList(), ); } @@ -258,6 +259,7 @@ class _RemoteMenubarState extends State { height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) + .expand((i) => i) .toList(), ); } @@ -401,7 +403,7 @@ class _RemoteMenubarState extends State { List> _getDisplayMenu() { final displayMenu = [ - MenuEntrySubRadios( + MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ Tuple2(translate('Original'), 'original'), @@ -418,7 +420,8 @@ class _RemoteMenubarState extends State { id: widget.id, name: "view-style", value: v); widget.ffi.canvasModel.updateViewStyle(); }), - MenuEntrySubRadios( + MenuEntryDivider(), + MenuEntryRadios( text: translate('Scroll Style'), optionsGetter: () => [ Tuple2(translate('ScrollAuto'), 'scrollauto'), @@ -434,7 +437,8 @@ class _RemoteMenubarState extends State { id: widget.id, name: "scroll-style", value: v); widget.ffi.canvasModel.updateScrollStyle(); }), - MenuEntrySubRadios( + MenuEntryDivider(), + MenuEntryRadios( text: translate('Image Quality'), optionsGetter: () => [ Tuple2(translate('Good image quality'), 'best'), @@ -451,6 +455,7 @@ class _RemoteMenubarState extends State { optionSetter: (String v) async { await bind.sessionSetImageQuality(id: widget.id, value: v); }), + MenuEntryDivider(), MenuEntrySwitch( text: translate('Show remote cursor'), getter: () async { diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fc59b8bd3..a35f1c872 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -40,10 +40,7 @@ dependencies: 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 From b7ce85e0626e40e8b1c29cfa0e415ca2a6435ce9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 30 Aug 2022 21:15:35 +0800 Subject: [PATCH 16/35] flutter_deskop: sync session add, mid commit Signed-off-by: fufesou --- flutter/lib/models/model.dart | 4 +- src/client.rs | 2 +- src/flutter.rs | 69 +++++++++++++++++++++-------------- src/flutter_ffi.rs | 27 +++++++++----- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4a54ba4e8..171a41dfa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1070,8 +1070,10 @@ class FFI { imageModel._id = id; cursorModel.id = id; } - final stream = bind.sessionConnect( + // 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) { diff --git a/src/client.rs b/src/client.rs index d7f0bf4fa..0bc69a7c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -847,7 +847,7 @@ impl VideoHandler { pub struct LoginConfigHandler { id: String, pub is_file_transfer: bool, - is_port_forward: bool, + pub is_port_forward: bool, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, diff --git a/src/flutter.rs b/src/flutter.rs index 7192c0fdf..60650aa9b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,16 +8,13 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::config::{PeerConfig, TransferSerde}; -use hbb_common::fs::get_job; use hbb_common::{ - allow_err, + allow_err, bail, compress::decompress, - config::{Config, LocalConfig}, - fs, + config::{Config, LocalConfig, PeerConfig, TransferSerde}, fs::{ - can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path, - DigestCheckResult, + self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, + transform_windows_path, DigestCheckResult, }, log, message_proto::*, @@ -28,7 +25,7 @@ use hbb_common::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }, - Stream, + ResultType, Stream, }; use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; @@ -60,7 +57,7 @@ pub struct Session { id: String, sender: Arc>>>, // UI to rust lc: Arc>, - events2ui: Arc>>, + events2ui: Arc>>>, } impl Session { @@ -71,23 +68,17 @@ impl Session { /// * `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 start( - identifier: &str, - is_file_transfer: bool, - is_port_forward: bool, - events2ui: StreamSink, - ) { + pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { // TODO check same id - let session_id = get_session_id(identifier.to_owned()); + let session_id = get_session_id(id.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, + events2ui: Arc::new(RwLock::new(None)), }; session.lc.write().unwrap().initialize( session_id.clone(), @@ -97,10 +88,29 @@ impl Session { SESSIONS .write() .unwrap() - .insert(identifier.to_owned(), session.clone()); - std::thread::spawn(move || { - Connection::start(session, is_file_transfer, is_port_forward); - }); + .insert(id.to_owned(), session.clone()); + Ok(()) + } + + /// Create a new remote 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 start(id: &str, events2ui: StreamSink) -> ResultType<()> { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + *session.events2ui.write().unwrap() = Some(events2ui); + let session = session.clone(); + std::thread::spawn(move || { + let is_file_transfer = session.lc.read().unwrap().is_file_transfer; + let is_port_forward = session.lc.read().unwrap().is_port_forward; + Connection::start(session, is_file_transfer, is_port_forward); + }); + Ok(()) + } else { + bail!("No session with peer id {}", id) + } } /// Get the current session instance. @@ -305,7 +315,9 @@ 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)); + if let Some(stream) = &*self.events2ui.read().unwrap() { + stream.add(EventToUI::Event(out)); + } } /// Get platform of peer. @@ -998,11 +1010,12 @@ impl Connection { }) }; 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(), - ))); + if let Some(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)) => { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index dd147bb77..db8030782 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -107,14 +107,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, - is_port_forward: bool, -) -> ResultType<()> { - Session::start(&id, is_file_transfer, is_port_forward, 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 { @@ -602,7 +606,12 @@ pub fn main_load_lan_peers() { }; } -pub fn session_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { +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); } From 7fce02e68822ed1293c70591d4039bf2b08ac1fa Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 29 Aug 2022 19:28:00 +0800 Subject: [PATCH 17/35] fix: not use fixed button width Signed-off-by: 21pages --- flutter/lib/desktop/pages/connection_page.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 29219df2a..d366d06ec 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -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), ), ), ), From 839be76b8f890919be69c36a8ae56f5672508df8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 30 Aug 2022 14:43:57 +0800 Subject: [PATCH 18/35] tabbar: check before scroll Signed-off-by: 21pages --- flutter/lib/desktop/widgets/tabbar_widget.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 6e0ce747d..aea90c868 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -64,6 +64,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); } @@ -96,8 +97,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); } From 38abd273840723bd7caee72b201c8444283f9a18 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 30 Aug 2022 16:50:25 +0800 Subject: [PATCH 19/35] impl option remote modification Signed-off-by: 21pages --- flutter/lib/common.dart | 2 - .../desktop/pages/connection_tab_page.dart | 2 +- .../lib/desktop/pages/desktop_tab_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 6 +- .../desktop/pages/port_forward_tab_page.dart | 10 +-- flutter/lib/desktop/pages/server_page.dart | 26 ++++++-- .../lib/desktop/widgets/tabbar_widget.dart | 62 +++++++++++++++++-- flutter/lib/main.dart | 9 ++- src/flutter_ffi.rs | 23 ++++--- 9 files changed, 109 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b991c7a96..6ba917bf3 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -664,8 +664,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/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 1b9e04ebf..4175bd11b 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -93,7 +93,7 @@ class _ConnectionTabPageState extends State { body: Obx(() => DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.remoteScreen, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index a7a93d7ad..57ee43e14 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -46,7 +46,7 @@ class _DesktopTabPageState extends State { body: DesktopTab( controller: tabController, theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - isMainWindow: true, + tabType: DesktopTabType.main, tail: ActionIcon( message: 'Settings', icon: IconFont.menu, diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index e7f08a516..6c8b58a30 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -37,7 +37,7 @@ class _FileManagerTabPageState extends State { @override void initState() { super.initState(); - + tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { @@ -74,9 +74,9 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.fileTransfer, onClose: () { - tabController.clear(); + tabController.clear(); }, tail: AddButton( theme: theme, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 8db4c7f98..1e2c8e2bc 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -19,11 +19,13 @@ class PortForwardTabPage extends StatefulWidget { class _PortForwardTabPageState extends State { final tabController = Get.put(DesktopTabController()); + late final bool isRDP; - static final IconData selectedIcon = Icons.forward_sharp; - static final IconData unselectedIcon = Icons.forward_outlined; + 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'], @@ -32,7 +34,7 @@ class _PortForwardTabPageState extends State { page: PortForwardPage( key: ValueKey(params['id']), id: params['id'], - isRDP: params['isRDP'], + isRDP: isRDP, ))); } @@ -76,7 +78,7 @@ class _PortForwardTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward, onClose: () { tabController.clear(); }, 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/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index aea90c868..a89d0da38 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -6,6 +7,7 @@ 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:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; @@ -34,6 +36,15 @@ class TabInfo { required this.page}); } +enum DesktopTabType { + main, + cm, + remoteScreen, + fileTransfer, + portForward, + rdp, +} + class DesktopTabState { final List tabs = []; final ScrollPosController scrollController = @@ -143,6 +154,7 @@ 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; @@ -161,7 +173,7 @@ class DesktopTab extends StatelessWidget { const DesktopTab({ required this.controller, - required this.isMainWindow, + required this.tabType, this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, @@ -175,7 +187,8 @@ class DesktopTab extends StatelessWidget { this.onClose, this.tabBuilder, this.labelGetter, - }); + }) : isMainWindow = + tabType == DesktopTabType.main || tabType == DesktopTabType.cm; @override Widget build(BuildContext context) { @@ -204,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() { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index efca88180..e1c254942 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -81,6 +81,8 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { await initEnv(kAppTypeMain); + // trigger connection status updater + await bind.mainCheckConnectStatus(); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -89,10 +91,11 @@ void runMainApp(bool startService) async { } runApp(App()); // set window option - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(const Size(1280, 720)); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(const Size(1280, 720)); windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); + await windowManager.show(); + await windowManager.focus(); }); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index db8030782..a17b3d695 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,12 +22,13 @@ 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) { @@ -472,7 +473,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 { @@ -764,6 +765,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); } From bdcb848a7562ec899b7ea1c0656056ef5f68ce06 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 31 Aug 2022 16:31:31 +0800 Subject: [PATCH 20/35] refactor remote interface --- src/client.rs | 6 + src/client/file_trait.rs | 24 +- src/flutter.rs | 74 +-- src/lib.rs | 1 + src/ui.rs | 2 +- src/ui/remote.rs | 870 ++++++++++++++++++++++-------------- src/ui_session_interface.rs | 257 +++++++++++ 7 files changed, 826 insertions(+), 408 deletions(-) create mode 100644 src/ui_session_interface.rs diff --git a/src/client.rs b/src/client.rs index 0bc69a7c1..64c7daf4d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1651,6 +1651,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/flutter.rs b/src/flutter.rs index 60650aa9b..392f0f733 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -39,8 +39,9 @@ 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"; +const MILLI1: Duration = Duration::from_millis(1); + lazy_static::lazy_static! { - // static ref SESSION: Arc>> = Default::default(); pub static ref SESSIONS: RwLock> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } @@ -48,9 +49,6 @@ lazy_static::lazy_static! { 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 { @@ -113,10 +111,6 @@ impl Session { } } - /// Get the current session instance. - // pub fn get() -> Arc>> { - // SESSION.clone() - // } /// Get the option of the current session. /// @@ -252,57 +246,6 @@ impl Session { 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); - // } - // } - /// Push an event to the event queue. /// An event is stored as json in the event queue. /// @@ -595,6 +538,18 @@ impl Interface for Session { } } + fn is_file_transfer(&self) -> bool { + todo!() + } + + fn is_port_forward(&self) -> bool { + todo!() + } + + fn is_rdp(&self) -> bool { + todo!() + } + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { let has_retry = if check_if_retry(msgtype, title, text) { "true" @@ -706,7 +661,6 @@ impl Interface for Session { } } -const MILLI1: Duration = Duration::from_millis(1); struct Connection { video_handler: VideoHandler, 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/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 a0245c28a..f5abb3d73 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,6 +1,6 @@ use std::{ collections::HashMap, - ops::Deref, + ops::{Deref, DerefMut}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, Mutex, RwLock, @@ -48,6 +48,7 @@ use crate::clipboard_file::*; use crate::{ client::*, common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::{InvokeUi, Session}, }; use errno; @@ -74,46 +75,200 @@ 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>>, + thread: 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)); + } + + fn update_privacy_mode(&self) { + self.call("updatePrivacyMode", &[]); + } + + fn set_permission(&self, name: &str, value: bool) { + self.call2("setPermission", &make_args!(name, value)); + } + + fn update_pi(&self, pi: PeerInfo) {} + + 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) { + todo!() + } + + fn job_done(&self, id: i32, file_num: i32) { + todo!() + } + + 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) { + todo!() + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + todo!() + } + + fn adapt_size(&self) { + self.call("adaptSize", &make_args!()); + } +} + +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 + } +} + +// #[derive(Default)] +// pub struct HandlerInner { +// element: Option, +// sender: Option>, +// thread: Option>, +// close_state: HashMap, +// } + +// #[derive(Clone, Default)] +// pub struct Handler { +// inner: Arc>, +// cmd: String, +// id: String, +// password: String, +// args: Vec, +// lc: Arc>, +// } + +// impl Deref for Handler { +// type Target = Arc>; + +// fn deref(&self) -> &Self::Target { +// &self.inner +// } +// } + +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(); }); } @@ -239,38 +394,40 @@ impl sciter::EventHandler for Handler { } } -impl Handler { +impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { - let me = Self { + let session: Session = Session { cmd, 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()) - ), + session.lc.write().unwrap().initialize( + id, + session.is_file_transfer(), + session.is_port_forward(), ); + + Self(session) } - fn start_keyboard_hook(&self) { + // 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 start_keyboard_hook(&'static self) { if self.is_port_forward() || self.is_file_transfer() { return; } @@ -278,7 +435,7 @@ impl Handler { return; } log::info!("keyboard hooked"); - let mut me = self.clone(); + let me = self.clone(); let peer = self.peer_platform(); let is_win = peer == "Windows"; #[cfg(windows)] @@ -364,7 +521,7 @@ impl Handler { Key::UpArrow => Some(ControlKey::UpArrow), Key::Delete => { if is_win && ctrl && alt { - me.ctrl_alt_del(); + // me.ctrl_alt_del(); // TODO return; } Some(ControlKey::Delete) @@ -485,7 +642,7 @@ impl Handler { } if chr != '\0' { if chr == 'l' && is_win && command { - me.lock_screen(); + // me.lock_screen(); // TODO return; } key_event.set_chr(chr as _); @@ -494,7 +651,7 @@ impl Handler { return; } } - me.key_down_or_up(down, key_event, alt, ctrl, shift, command); + // me.key_down_or_up(down, key_event, alt, ctrl, shift, command); // TODO }; if let Err(error) = rdev::listen(func) { log::error!("rdev: {:?}", error); @@ -518,19 +675,19 @@ impl Handler { v } - #[inline] - pub(super) fn save_config(&self, config: PeerConfig) { - self.lc.write().unwrap().save_config(config); - } + // #[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) - } + // #[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()); @@ -635,9 +792,9 @@ impl Handler { self.send(Data::Message(msg)); } - pub fn is_restarting_remote_device(&self) -> bool { - self.lc.read().unwrap().restarting_remote_device - } + // 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) @@ -672,7 +829,7 @@ impl Handler { 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" { @@ -785,20 +942,20 @@ impl Handler { pi } - fn get_option(&self, k: String) -> String { - self.lc.read().unwrap().get_option(&k) - } + // 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 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 save_close_state(&mut self, k: String, v: String) { + self.close_state.insert(k, v); } fn get_chatbox(&mut self) -> String { @@ -834,49 +991,49 @@ impl Handler { self.send(Data::Message(msg_out)); } - fn is_file_transfer(&self) -> bool { - self.cmd == "--file-transfer" - } + // 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_port_forward(&self) -> bool { + // self.cmd == "--port-forward" || self.is_rdp() + // } - fn is_rdp(&self) -> bool { - self.cmd == "--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 || { + let mut lock = self.thread.lock().unwrap(); + lock.take().map(|t| t.join()); + *lock = Some(std::thread::spawn(move || { io_loop(cloned); })); } - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() - } + // #[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_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_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(); @@ -967,7 +1124,7 @@ impl Handler { } } - send_mouse(mask, x, y, alt, ctrl, shift, command, self); + send_mouse(mask, x, y, alt, ctrl, shift, command, &self.0); // 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 @@ -1210,42 +1367,26 @@ impl Handler { self.send(Data::Message(msg_out)); } - #[inline] - fn set_cursor_id(&mut self, id: String) { - self.call("setCursorId", &make_args!(id)); - } + // #[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 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)); - } + // #[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, +async fn start_one_port_forward( + handler: Session, port: i32, remote_host: String, remote_port: i32, @@ -1273,9 +1414,9 @@ async fn start_one_port_forward( } #[tokio::main(flavor = "current_thread")] -async fn io_loop(handler: Handler) { +async fn io_loop(handler: Session) { let (sender, mut receiver) = mpsc::unbounded_channel::(); - handler.write().unwrap().sender = Some(sender.clone()); + *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"); @@ -1431,8 +1572,8 @@ impl RemoveJob { } } -struct Remote { - handler: Handler, +struct Remote { + handler: Session, video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, @@ -1451,7 +1592,7 @@ struct Remote { video_format: CodecFormat, } -impl Remote { +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(); @@ -1474,8 +1615,9 @@ impl Remote { 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)); + // self.handler + // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + self.handler.set_connection_type(peer.is_secured(), direct); // just build for now #[cfg(not(windows))] @@ -1597,10 +1739,12 @@ impl Remote { } } if let Some(err) = err { - self.handler - .call("jobError", &make_args!(id, err, file_num)); + // self.handler + // .call("jobError", &make_args!(id, err, file_num)); + self.handler.job_error(id, err, file_num); } else { - self.handler.call("jobDone", &make_args!(id, file_num)); + // self.handler.call("jobDone", &make_args!(id, file_num)); + self.handler.job_done(id, file_num); } } @@ -1645,7 +1789,8 @@ impl Remote { async fn load_last_jobs(&mut self) { log::info!("start load last jobs"); - self.handler.call("clearAllJobs", &make_args!()); + // self.handler.call("clearAllJobs", &make_args!()); + 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 @@ -1656,16 +1801,24 @@ impl Remote { 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 - ), + // self.handler.call( + // "addJob", + // &make_args!( + // cnt, + // job.to.clone(), + // job.remote.clone(), + // job.file_num, + // job.show_hidden, + // false + // ), + // ); + 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); @@ -1674,22 +1827,31 @@ impl Remote { 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 - ), + // self.handler.call( + // "addJob", + // &make_args!( + // cnt, + // job.remote.clone(), + // job.to.clone(), + // job.file_num, + // job.show_hidden, + // true + // ), + // ); + 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.call("updateTransferList", &make_args!()); + // self.handler.call("updateTransferList", &make_args!()); + self.handler.update_transfer_list(); } async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { @@ -1753,8 +1915,8 @@ impl Remote { to, job.files().len() ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); + // 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)] @@ -1813,8 +1975,8 @@ impl Remote { to, job.files().len() ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); + // 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); @@ -1860,10 +2022,11 @@ impl Remote { 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()), - ); + // self.handler.call( + // "confirmDeleteFiles", + // &make_args!(id, file_num, job.files[i].name.clone()), + // ); + self.handler.confirm_delete_files(id, file_num); } } } @@ -1924,8 +2087,8 @@ impl 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)); + // 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)); } @@ -2018,7 +2181,7 @@ impl Remote { job: &fs::TransferJob, elapsed: i32, last_update_jobs_status: &mut (Instant, HashMap), - handler: &mut Handler, + handler: &mut Session, ) { if elapsed <= 0 { return; @@ -2034,10 +2197,11 @@ impl Remote { 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), - ); + // handler.call( + // "jobProgress", + // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), + // ); + handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); } fn update_jobs_status(&mut self) { @@ -2103,8 +2267,10 @@ impl Remote { 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.handler.call2("closeSuccess", &make_args!()); + self.handler.close_success(); + // self.handler.call("adaptSize", &make_args!()); + self.handler.adapt_size(); self.send_opts_after_login(peer).await; } let incomming_format = CodecFormat::from(&vf); @@ -2192,11 +2358,11 @@ impl Remote { 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)); + // 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); @@ -2227,14 +2393,20 @@ impl Remote { 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 - ), + // self.handler.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // read_path, + // true + // ), + // ); + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, ); } } @@ -2271,14 +2443,20 @@ impl Remote { ); allow_err!(peer.send(&msg).await); } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - write_path, - false - ), + // self.handler.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // write_path, + // false + // ), + // ); + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, ); } } @@ -2333,24 +2511,27 @@ impl Remote { self.audio_sender.send(MediaData::AudioFormat(f)).ok(); } Some(misc::Union::ChatMessage(c)) => { - self.handler.call("newMessage", &make_args!(c.text)); + // self.handler.call("newMessage", &make_args!(c.text)); // TODO } 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)); + // self.handler + // .call2("setPermission", &make_args!("keyboard", p.enabled)); + self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - self.handler - .call2("setPermission", &make_args!("clipboard", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("clipboard", p.enabled)); + self.handler.set_permission("clipboard", p.enabled); } Permission::Audio => { - self.handler - .call2("setPermission", &make_args!("audio", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("audio", p.enabled)); + self.handler.set_permission("audio", p.enabled); } Permission::File => { SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); @@ -2358,17 +2539,19 @@ impl Remote { return true; } self.check_clipboard_file_context(); - self.handler - .call2("setPermission", &make_args!("file", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("file", p.enabled)); + self.handler.set_permission("file", p.enabled); } Permission::Restart => { - self.handler - .call2("setPermission", &make_args!("restart", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("restart", p.enabled)); + self.handler.set_permission("restart", p.enabled); } } } Some(misc::Union::SwitchDisplay(s)) => { - self.handler.call("switchDisplay", &make_args!(s.display)); + // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { VIDEO.lock().unwrap().as_mut().map(|v| { @@ -2441,7 +2624,7 @@ impl Remote { #[inline(always)] fn update_block_input_state(&mut self, on: bool) { - self.handler.call("updateBlockInputState", &make_args!(on)); + // self.handler.call("updateBlockInputState", &make_args!(on)); // TODO } async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { @@ -2471,7 +2654,8 @@ impl Remote { config.privacy_mode = on; self.handler.save_config(config); - self.handler.call("updatePrivacyMode", &[]); + // self.handler.call("updatePrivacyMode", &[]); + self.handler.update_privacy_mode(); } async fn handle_back_msg_privacy_mode( @@ -2593,143 +2777,137 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { 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(); - } - } +// #[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 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_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(); - } +// 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_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_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; - } - } +// 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 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); - } -} +// fn is_force_relay(&self) -> bool { +// self.lc.read().unwrap().force_relay +// } +// } #[tokio::main(flavor = "current_thread")] async fn send_note(url: String, id: String, conn_id: i32, note: String) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs new file mode 100644 index 000000000..8f7a0b904 --- /dev/null +++ b/src/ui_session_interface.rs @@ -0,0 +1,257 @@ +use crate::client::{ + self, check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, + FileManager, LoginConfigHandler, QualityStatus, load_config, +}; +use crate::{client::Data, client::Interface}; +use async_trait::async_trait; +use hbb_common::config::PeerConfig; +use hbb_common::message_proto::{CursorData, Hash, PeerInfo, TestDelay, CursorPosition}; +use hbb_common::tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, +}; +use hbb_common::{get_version_number, log, Stream}; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, RwLock}; + +#[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 ui_handler: T, +} + +impl Session { + pub fn get_option(&self, k: String) -> String { + self.lc.read().unwrap().get_option(&k) + } + + pub fn set_option(&self, k: String, v: String) { + self.lc.write().unwrap().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 get_platform(&mut self, is_remote: bool) -> String { + if is_remote { + self.peer_platform() + } else { + whoami::platform().to_string() + } + } + + pub fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + let p = self.get_platform(is_remote); + if &p == "Windows" { + return "\\"; + } else { + return "/"; + } + } +} + +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 update_privacy_mode(&self); + fn set_permission(&self, name: &str, value: bool); + fn update_pi(&self, pi: PeerInfo); + 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 update_transfer_list(&self); + // fn update_folder_files(&self); // TODO + 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 job_progress(&self, id:i32, file_num:i32, speed:f64, finished_size:f64); + fn adapt_size(&self); +} + + +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.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 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.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(username, pi); + self.update_privacy_mode(); + 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.update_privacy_mode(); + // self.update_pi(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); + } + } + // self.start_keyboard_hook(); // TODO + } + + 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 + } +} From 8f6fed5416eae311413b4a17fa1504d9897ed931 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 31 Aug 2022 19:23:32 +0800 Subject: [PATCH 21/35] fix tabbar close button can't show when selected && hovered Signed-off-by: 21pages --- .../lib/desktop/widgets/tabbar_widget.dart | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index a89d0da38..38e724bad 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -484,7 +484,7 @@ class _ListView extends StatelessWidget { } } -class _Tab extends StatelessWidget { +class _Tab extends StatefulWidget { late final int index; late final Rx label; late final IconData? selectedIcon; @@ -493,7 +493,6 @@ class _Tab extends StatelessWidget { 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; @@ -512,31 +511,39 @@ 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 = selectedIcon != null && unselectedIcon != null; - bool isSelected = index == selected; + bool showIcon = + widget.selectedIcon != null && widget.unselectedIcon != null; + bool isSelected = widget.index == widget.selected; final icon = Offstage( offstage: !showIcon, child: Icon( - isSelected ? selectedIcon : unselectedIcon, + isSelected ? widget.selectedIcon : widget.unselectedIcon, size: _kIconSize, color: isSelected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, + ? widget.theme.selectedtabIconColor + : widget.theme.unSelectedtabIconColor, ).paddingOnly(right: 5)); final labelWidget = Obx(() { return Text( - translate(label.value), + translate(widget.label.value), textAlign: TextAlign.center, style: TextStyle( color: isSelected - ? theme.selectedTextColor - : theme.unSelectedTextColor), + ? widget.theme.selectedTextColor + : widget.theme.unSelectedTextColor), ); }); - if (tabBuilder == null) { + if (widget.tabBuilder == null) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -545,37 +552,38 @@ class _Tab extends StatelessWidget { ], ); } else { - return tabBuilder!( - icon, labelWidget, TabThemeConf(iconSize: _kIconSize, theme: theme)); + return widget.tabBuilder!(icon, labelWidget, + TabThemeConf(iconSize: _kIconSize, theme: widget.theme)); } } @override Widget build(BuildContext context) { - bool showIcon = selectedIcon != null && unselectedIcon != null; - bool isSelected = index == selected; - bool showDivider = 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: [ _buildTabContent(), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: isSelected, - onClose: () => onClose(), - theme: theme, - ))), - ) + Obx((() => _CloseButton( + visiable: hover.value && widget.closable, + tabSelected: isSelected, + onClose: () => widget.onClose(), + theme: widget.theme, + ))) ])).paddingSymmetric(horizontal: 10), Offstage( offstage: !showDivider, @@ -583,7 +591,7 @@ class _Tab extends StatelessWidget { width: 1, indent: _kDividerIndent, endIndent: _kDividerIndent, - color: theme.dividerColor, + color: widget.theme.dividerColor, thickness: 1, ), ) @@ -592,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 { @@ -615,7 +631,7 @@ class _CloseButton extends StatelessWidget { child: Offstage( offstage: !visiable, child: InkWell( - customBorder: RoundedRectangleBorder(), + customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), child: Icon( Icons.close, From 3e8f7ed36df60045fc98f1cc989151548442a141 Mon Sep 17 00:00:00 2001 From: XieJiSS Date: Wed, 31 Aug 2022 20:24:48 +0800 Subject: [PATCH 22/35] fix: unicode-related error during .ts generation The user may uses a different codepage/encoding which is not unicode, so we'd like to get rid of that --- flutter/web/js/gen_js_from_hbb.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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) From e5c45542218daa8280e5054f317b3ab18093a7d1 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 31 Aug 2022 20:46:30 +0800 Subject: [PATCH 23/35] refactor remote, sciter / flutter run success --- src/flutter.rs | 3483 +++++++++++++++++++---------------- src/flutter_ffi.rs | 36 +- src/ui/remote.rs | 2027 +++----------------- src/ui_session_interface.rs | 1767 +++++++++++++++++- 4 files changed, 3888 insertions(+), 3425 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 392f0f733..88c6f1961 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -28,7 +28,10 @@ use hbb_common::{ ResultType, Stream, }; -use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; +use crate::{ + common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}, + ui_session_interface::{io_loop, InvokeUi, Session}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; @@ -42,210 +45,19 @@ pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; const MILLI1: Duration = Duration::from_millis(1); lazy_static::lazy_static! { - 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 } static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); - -#[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. - /// * `is_port_forward` - If the session is used for port forward. - pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { - // TODO check same id - let session_id = get_session_id(id.to_owned()); - LocalConfig::set_remote_id(&session_id); - // TODO close - // Self::close(); - let session = Session { - id: session_id.clone(), - sender: Default::default(), - lc: Default::default(), - events2ui: Arc::new(RwLock::new(None)), - }; - session.lc.write().unwrap().initialize( - session_id.clone(), - is_file_transfer, - is_port_forward, - ); - SESSIONS - .write() - .unwrap() - .insert(id.to_owned(), session.clone()); - Ok(()) - } - - /// Create a new remote 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 start(id: &str, events2ui: StreamSink) -> ResultType<()> { - if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - *session.events2ui.write().unwrap() = Some(events2ui); - let session = session.clone(); - std::thread::spawn(move || { - let is_file_transfer = session.lc.read().unwrap().is_file_transfer; - let is_port_forward = session.lc.read().unwrap().is_port_forward; - Connection::start(session, is_file_transfer, is_port_forward); - }); - Ok(()) - } else { - bail!("No session with peer id {}", id) - } - } - - - /// 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, 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)); - } - - /// 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); - } - +impl FlutterHandler { /// Push an event to the event queue. /// An event is stored as json in the event queue. /// @@ -258,221 +70,60 @@ impl Session { assert!(h.get("name").is_none()); h.insert("name", name); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); - if let Some(stream) = &*self.events2ui.read().unwrap() { + if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Event(out)); } } +} - /// Get platform of peer. - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() +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()), + ), + ], + ); } - /// 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); - } + fn set_cursor_id(&self, id: String) { + self.push_event("cursor_id", vec![("id", &id.to_string())]); } - /// 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); + fn set_cursor_position(&self, cp: CursorPosition) { + self.push_event( + "cursor_position", + vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], + ); } - /// 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_display(&self, x: i32, y: i32, w: i32, h: i32) { + // todo!() } - /// 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 update_privacy_mode(&self) { + self.push_event("update_privacy_mode", [].into()); } - /// 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 set_permission(&self, name: &str, value: bool) { + // todo!() } - 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); + fn update_pi(&self, pi: PeerInfo) { + // todo!() } - 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) { + // todo!() } fn update_quality_status(&self, status: QualityStatus) { @@ -495,67 +146,95 @@ impl Session { ); } - pub 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 set_connection_type(&self, is_secured: bool, direct: bool) { + self.push_event( + "connection_ready", + vec![ + ("secure", &is_secured.to_string()), + ("direct", &direct.to_string()), + ], + ); } - 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)); + fn job_error(&self, id: i32, err: String, file_num: i32) { + // todo!() } - fn on_error(&self, err: &str) { - self.msgbox("error", "Error", err); + fn job_done(&self, id: i32, file_num: i32) { + // todo!() } -} -impl FileManager for Session {} + fn clear_all_jobs(&self) { + // todo!() + } -#[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 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) { + // todo!() + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + // todo!() + } + + fn adapt_size(&self) { + // todo!() + } + + fn on_rgba(&self, data: &[u8]) { + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); } } - fn is_file_transfer(&self) -> bool { - todo!() + fn set_peer_info( + &self, + username: &str, + hostname: &str, + platform: &str, + sas_enabled: bool, + displays: &Vec>, + version: &str, + current_display: usize, + is_file_transfer: bool, + ) { + let displays = serde_json::ser::to_string(displays).unwrap_or("".to_owned()); + self.push_event( + "peer_info", + vec![ + ("username", username), + ("hostname", hostname), + ("platform", platform), + ("sas_enabled", &sas_enabled.to_string()), + ("displays", &displays), + ("version", &version), + ("current_display", ¤t_display.to_string()), + ("is_file_transfer", &is_file_transfer.to_string()), + ], + ); } - fn is_port_forward(&self) -> bool { - todo!() - } - - fn is_rdp(&self) -> bool { - todo!() - } - - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { - let has_retry = if check_if_retry(msgtype, title, text) { - "true" - } else { - "" - }; + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + let has_retry = if retry { "true" } else { "" }; self.push_event( "msgbox", vec![ @@ -566,1184 +245,1754 @@ impl Interface for Session { ], ); } +} - fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) - } +/// 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<()> { + // TODO check same id + let session_id = get_session_id(id.to_owned()); + LocalConfig::set_remote_id(&session_id); + // TODO close + // Self::close(); - 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; + // TODO cmd passwd args + let session: Session = Session { + id: session_id.clone(), + ..Default::default() + }; - 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()); - self.push_event( - "peer_info", - 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()), - ], - ); - lc.handle_peer_info(username, pi); - let p = lc.should_auto_login(); - if !p.is_empty() { - input_os_password(p, true, self.clone()); - } - } + session + .lc + .write() + .unwrap() + .initialize(session_id.clone(), is_file_transfer, is_port_forward); + SESSIONS + .write() + .unwrap() + .insert(id.to_owned(), session.clone()); + Ok(()) +} - 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; - } +/// 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 || { + // let is_file_transfer = session.lc.read().unwrap().is_file_transfer; + // let is_port_forward = session.lc.read().unwrap().is_port_forward; + // Connection::start(session, is_file_transfer, is_port_forward); + io_loop(session); + }); + Ok(()) + } else { + bail!("No session with peer id {}", id) } } +// #[derive(Clone)] +// pub struct Session { +// id: String, +// sender: Arc>>>, // UI to rust +// lc: Arc>, +// events2ui: Arc>>>, +// } -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 Session1 { +// /// 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 add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { +// // TODO check same id +// let session_id = get_session_id(id.to_owned()); +// LocalConfig::set_remote_id(&session_id); +// // TODO close +// // Self::close(); +// let session = Session { +// id: session_id.clone(), +// sender: Default::default(), +// lc: Default::default(), +// events2ui: Arc::new(RwLock::new(None)), +// }; +// session.lc.write().unwrap().initialize( +// session_id.clone(), +// is_file_transfer, +// is_port_forward, +// ); +// SESSIONS +// .write() +// .unwrap() +// .insert(id.to_owned(), session.clone()); +// Ok(()) +// } -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 remote 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 start(id: &str, events2ui: StreamSink) -> ResultType<()> { +// if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { +// *session.events2ui.write().unwrap() = Some(events2ui); +// let session = session.clone(); +// std::thread::spawn(move || { +// let is_file_transfer = session.lc.read().unwrap().is_file_transfer; +// let is_port_forward = session.lc.read().unwrap().is_port_forward; +// Connection::start(session, is_file_transfer, is_port_forward); +// }); +// Ok(()) +// } else { +// bail!("No session with peer id {}", id) +// } +// } - /// Create a new connection. - /// - /// # Arguments - /// - /// * `session` - The session to create a new connection for. - /// * `is_file_transfer` - Whether the connection is for file transfer. - /// * `is_port_forward` - Whether the connection is for port forward. - #[tokio::main(flavor = "current_thread")] - async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { - let mut last_recv_time = Instant::now(); - let (sender, mut receiver) = mpsc::unbounded_channel::(); - let mut stop_clipboard = None; - if !is_file_transfer && !is_port_forward { - stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); - } - *session.sender.write().unwrap() = Some(sender.clone()); - let conn_type = if is_file_transfer { - session.lc.write().unwrap().is_file_transfer = true; - ConnType::FILE_TRANSFER - } else if is_port_forward { - ConnType::PORT_FORWARD // TODO: RDP - } else { - ConnType::DEFAULT_CONN - }; - let key = Config::get_option("key"); - let token = Config::get_option("access_token"); +// /// 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) +// } - // TODO rdp & cli args - let is_rdp = false; - let args: Vec = Vec::new(); +// /// 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); +// } - if is_port_forward { - if 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 args.len() == 0 { - let pfs = session.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 = session.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; - } +// /// Input the OS password. +// pub fn input_os_password(&self, pass: String, activate: bool) { +// input_os_password(pass, activate, self.clone()); +// } - let latency_controller = LatencyController::new(); - let latency_controller_cl = latency_controller.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); +// } - 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, - }; +// /// 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); +// } +// } - 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); +// /// Send a refresh command. +// pub fn refresh(&self) { +// self.send(Data::Message(LoginConfigHandler::refresh())); +// } - session.push_event( - "connection_ready", - vec![ - ("secure", &peer.is_secured().to_string()), - ("direct", &direct.to_string()), - ], - ); +// /// Get image quality. +// pub fn get_image_quality(&self) -> String { +// self.lc.read().unwrap().image_quality.clone() +// } - let mut status_timer = time::interval(Duration::new(1, 0)); +// /// 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); +// } +// } - 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()); - } - } +// /// 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) +// } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - } +// /// 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))); +// } - /// 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) { - if let Some(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 - } +// /// Close the session. +// pub fn close(&self) { +// self.send(Data::Close); +// } - 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 - } +// /// 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, false); +// }); +// } - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - self.session.push_event( - "update_block_input_state", - [("input_state", if on { "on" } else { "off" })].into(), - ); - } +// /// Get `remember` flag in [`LoginConfigHandler`]. +// pub fn get_remember(&self) -> bool { +// self.lc.read().unwrap().remember +// } - 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"); - } - _ => {} - } - } +// /// 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)); +// } - #[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()); - } +// /// 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); +// } - 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 - } +// /// Push an event to the event queue. +// /// An event is stored as json in the event queue. +// /// +// /// # Arguments +// /// +// /// * `name` - The name of the event. +// /// * `event` - Fields of the event content. +// fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { +// let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); +// assert!(h.get("name").is_none()); +// h.insert("name", name); +// let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); +// if let Some(stream) = &*self.events2ui.read().unwrap() { +// stream.add(EventToUI::Event(out)); +// } +// } - 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 - } +// /// Get platform of peer. +// #[inline] +// fn peer_platform(&self) -> String { +// self.lc.read().unwrap().info.platform.clone() +// } - #[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()), - ], - ); - } +// /// 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); +// } +// } - 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(); - } - } +// /// 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); +// } - 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())], - ); - } - } +// /// 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 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()), - ], - ); - } +// /// 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); +// } +// } +// } - 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 - } -} +// /// 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 update_quality_status(&self, status: QualityStatus) { +// const NULL: String = String::new(); +// self.push_event( +// "update_quality_status", +// vec![ +// ("speed", &status.speed.map_or(NULL, |it| it)), +// ("fps", &status.fps.map_or(NULL, |it| it.to_string())), +// ("delay", &status.delay.map_or(NULL, |it| it.to_string())), +// ( +// "target_bitrate", +// &status.target_bitrate.map_or(NULL, |it| it.to_string()), +// ), +// ( +// "codec_format", +// &status.codec_format.map_or(NULL, |it| it.to_string()), +// ), +// ], +// ); +// } + +// pub 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)); +// } + +// 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)); +// } + +// fn on_error(&self, err: &str) { +// self.msgbox("error", "Error", err); +// } +// } + +// 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 { +// todo!() +// } + +// fn is_port_forward(&self) -> bool { +// todo!() +// } + +// fn is_rdp(&self) -> bool { +// todo!() +// } + +// fn msgbox(&self, msgtype: &str, title: &str, text: &str) { +// let has_retry = if check_if_retry(msgtype, title, text) { +// "true" +// } else { +// "" +// }; +// self.push_event( +// "msgbox", +// vec![ +// ("type", msgtype), +// ("title", title), +// ("text", text), +// ("hasRetry", has_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 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()); +// self.push_event( +// "peer_info", +// 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()), +// ], +// ); +// 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; +// } +// } +// } + +// 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. +// /// * `is_port_forward` - Whether the connection is for port forward. +// #[tokio::main(flavor = "current_thread")] +// async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { +// let mut last_recv_time = Instant::now(); +// let (sender, mut receiver) = mpsc::unbounded_channel::(); +// let mut stop_clipboard = None; +// if !is_file_transfer && !is_port_forward { +// stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); +// } +// *session.sender.write().unwrap() = Some(sender.clone()); +// let conn_type = if is_file_transfer { +// session.lc.write().unwrap().is_file_transfer = true; +// ConnType::FILE_TRANSFER +// } else if is_port_forward { +// ConnType::PORT_FORWARD // TODO: RDP +// } else { +// ConnType::DEFAULT_CONN +// }; +// let key = Config::get_option("key"); +// let token = Config::get_option("access_token"); + +// // TODO rdp & cli args +// let is_rdp = false; +// let args: Vec = Vec::new(); + +// if is_port_forward { +// if 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 args.len() == 0 { +// let pfs = session.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 = session.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 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, +// }; + +// 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) { +// if let Some(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( +// "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"); +// } +// _ => {} +// } +// } + +// #[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()); +// } + +// 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 +// } +// } // Server Side // TODO connection_manager need use struct and trait,impl default method @@ -2379,30 +2628,30 @@ pub fn get_session_id(id: String) -> String { }; } -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); -} +// 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); +// } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index db8030782..5226416b2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,10 +13,10 @@ 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")))] @@ -110,7 +110,7 @@ pub fn host_stop_system_key_propagate(stopped: bool) { // 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) { + 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()) @@ -118,7 +118,7 @@ pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: boo } pub fn session_start(events2ui: StreamSink, id: String) -> ResultType<()> { - Session::start(&id, events2ui) + session_start_(&id, events2ui) } pub fn session_get_remember(id: String) -> Option { @@ -131,7 +131,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 } @@ -152,7 +152,7 @@ pub fn session_get_image_quality(id: String) -> Option { 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 } @@ -160,7 +160,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); } } @@ -173,7 +173,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(); } } @@ -184,26 +184,26 @@ pub fn session_reconnect(id: String) { } pub fn session_toggle_option(id: String, value: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.toggle_option(&value); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.toggle_option(value); } } pub fn session_set_image_quality(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.set_image_quality(&value); + // session.set_image_quality(value); } } pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.lock_screen(); + // session.lock_screen(); } } pub fn session_ctrl_alt_del(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.ctrl_alt_del(); + // session.ctrl_alt_del(); } } @@ -224,13 +224,13 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.input_key(&name, down, press, alt, ctrl, shift, command); + // session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.input_string(&value); + // session.input_string(&value); } } @@ -249,7 +249,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() } @@ -348,7 +348,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!( @@ -719,7 +719,7 @@ pub fn session_send_mouse(id: String, msg: String) { } << 3; } if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.send_mouse(mask, x, y, alt, ctrl, shift, command); + // session.send_mouse(mask, x, y, alt, ctrl, shift, command); } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f5abb3d73..49812e09a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -48,7 +48,7 @@ use crate::clipboard_file::*; use crate::{ client::*, common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, - ui_session_interface::{InvokeUi, Session}, + ui_session_interface::{io_loop, InvokeUi, Remote, Session, SERVER_KEYBOARD_ENABLED}, }; use errno; @@ -69,9 +69,7 @@ fn get_key_state(key: enigo::Key) -> bool { 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; @@ -82,7 +80,6 @@ static mut IS_ALT_GR: bool = false; #[derive(Clone, Default)] pub struct SciterHandler { element: Arc>>, - thread: Arc>>>, close_state: HashMap, } @@ -128,6 +125,11 @@ impl InvokeUi for SciterHandler { fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { self.call("setDisplay", &make_args!(x, y, w, h)); + 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) { @@ -214,6 +216,32 @@ impl InvokeUi for SciterHandler { 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, + username: &str, + hostname: &str, + platform: &str, + sas_enabled: bool, + displays: &Vec>, + version: &str, + current_display: usize, + is_file_transfer: bool, + ) { + todo!() + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + todo!() + } } pub struct SciterSession(Session); @@ -231,32 +259,6 @@ impl DerefMut for SciterSession { } } -// #[derive(Default)] -// pub struct HandlerInner { -// element: Option, -// sender: Option>, -// thread: Option>, -// close_state: HashMap, -// } - -// #[derive(Clone, Default)] -// pub struct Handler { -// inner: Arc>, -// cmd: String, -// id: String, -// password: String, -// args: Vec, -// lc: Arc>, -// } - -// impl Deref for Handler { -// type Target = Arc>; - -// fn deref(&self) -> &Self::Target { -// &self.inner -// } -// } - impl sciter::EventHandler for SciterSession { fn get_subscription(&mut self) -> Option { Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) @@ -659,14 +661,15 @@ impl SciterSession { }); } - fn get_view_style(&mut self) -> String { - return self.lc.read().unwrap().view_style.clone(); - } + // 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_image_quality(&mut self) -> String { + // return self.lc.read().unwrap().image_quality.clone(); + // } + // TODO fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { @@ -680,82 +683,83 @@ impl SciterSession { // self.lc.write().unwrap().save_config(config); // } - fn save_view_style(&mut self, value: String) { - self.lc.write().unwrap().save_view_style(value); - } + // 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 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 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 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 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_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 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 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 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 has_hwcodec(&self) -> bool { + // #[cfg(not(feature = "hwcodec"))] + // return false; + // #[cfg(feature = "hwcodec")] + // return true; + // } + // TODO fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { @@ -780,51 +784,52 @@ impl SciterSession { } } - fn change_prefer_codec(&self) { - let msg = self.lc.write().unwrap().change_prefer_codec(); - self.send(Data::Message(msg)); - } + // 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)); - } + // 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 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 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 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 is_xfce(&self) -> bool { + // crate::platform::is_xfce() + // } + // TODO 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(); @@ -885,33 +890,33 @@ impl SciterSession { 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 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 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() { @@ -929,9 +934,9 @@ impl SciterSession { v } - fn get_id(&mut self) -> String { - self.id.clone() - } + // fn get_id(&mut self) -> String { + // self.id.clone() + // } fn get_default_pi(&mut self) -> Value { let mut pi = Value::map(); @@ -950,46 +955,47 @@ impl SciterSession { // 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 input_os_password(&mut self, pass: String, activate: bool) { + // input_os_password(pass, activate, self.clone()); + // } + // close_state sciter only fn save_close_state(&mut self, k: String, v: String) { self.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_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 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 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 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" @@ -1003,15 +1009,15 @@ impl SciterSession { // self.cmd == "--rdp" // } - fn reconnect(&mut self) { - println!("reconnecting"); - 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); - })); - } + // fn reconnect(&mut self) { + // println!("reconnecting"); + // 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); + // })); + // } // #[inline] // fn peer_platform(&self) -> String { @@ -1035,63 +1041,63 @@ impl SciterSession { // } // } - 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 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 login(&mut self, password: String, remember: bool) { + // self.send(Data::Login((password, remember))); + // } - fn new_rdp(&mut self) { - self.send(Data::NewRDP); - } + // fn new_rdp(&mut self) { + // self.send(Data::NewRDP); + // } fn enter(&mut self) { #[cfg(windows)] @@ -1383,1373 +1389,6 @@ impl SciterSession { // } } -const MILLI1: Duration = Duration::from_millis(1); - -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 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(); - } - 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: 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 { - 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)); - self.handler.set_connection_type(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)); - self.handler.job_error(id, err, file_num); - } else { - // self.handler.call("jobDone", &make_args!(id, file_num)); - 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(); - 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!()); - 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.call( - // "addJob", - // &make_args!( - // cnt, - // job.to.clone(), - // job.remote.clone(), - // job.file_num, - // job.show_hidden, - // false - // ), - // ); - 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.call( - // "addJob", - // &make_args!( - // cnt, - // job.remote.clone(), - // job.to.clone(), - // job.file_num, - // job.show_hidden, - // true - // ), - // ); - 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.call("updateTransferList", &make_args!()); - 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.call( - // "confirmDeleteFiles", - // &make_args!(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.call( - // "jobProgress", - // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - // ); - 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(); - } - } - - 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.close_success(); - // self.handler.call("adaptSize", &make_args!()); - 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 { - 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 - // ), - // ); - 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.call( - // "overrideFileConfirm", - // &make_args!( - // digest.id, - // digest.file_num, - // write_path, - // false - // ), - // ); - 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.call("newMessage", &make_args!(c.text)); // TODO - } - 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)); - self.handler.set_permission("keyboard", p.enabled); - } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - // self.handler - // .call2("setPermission", &make_args!("clipboard", p.enabled)); - self.handler.set_permission("clipboard", p.enabled); - } - Permission::Audio => { - // self.handler - // .call2("setPermission", &make_args!("audio", p.enabled)); - 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 - // .call2("setPermission", &make_args!("file", p.enabled)); - self.handler.set_permission("file", p.enabled); - } - Permission::Restart => { - // self.handler - // .call2("setPermission", &make_args!("restart", p.enabled)); - self.handler.set_permission("restart", p.enabled); - } - } - } - Some(misc::Union::SwitchDisplay(s)) => { - // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO - 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)); // TODO - } - - 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", &[]); - 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 - }; - } - } - } -} - pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { let mut m = Value::map(); m.set_item("id", id); @@ -2776,141 +1415,3 @@ 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 -// } -// } - -#[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); -} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 8f7a0b904..a8871fac1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,19 +1,33 @@ use crate::client::{ self, check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - FileManager, LoginConfigHandler, QualityStatus, load_config, + load_config, start_video_audio_threads, Client, CodecFormat, FileManager, LoginConfigHandler, + MediaData, MediaSender, QualityStatus, SEC30, }; +use crate::common::{ + self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, +}; +use crate::platform; use crate::{client::Data, client::Interface}; use async_trait::async_trait; -use hbb_common::config::PeerConfig; -use hbb_common::message_proto::{CursorData, Hash, PeerInfo, TestDelay, CursorPosition}; +use hbb_common::config::{Config, LocalConfig, 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::{get_version_number, log, Stream}; +use hbb_common::{allow_err, message_proto::*, sleep}; +use hbb_common::{fs, get_version_number, log, Stream}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, RwLock}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; #[derive(Clone, Default)] pub struct Session { @@ -23,18 +37,173 @@ pub struct Session { 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 { + return self.lc.read().unwrap().view_style.clone(); + } + + pub fn get_image_quality(&self) -> String { + return self.lc.read().unwrap().image_quality.clone(); + } + + 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 { + let res = self.lc.read().unwrap().get_toggle_option(&name); + return res; + } + + 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)); + } + + 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 { - self.lc.read().unwrap().get_option(&k) + let res = self.lc.read().unwrap().get_option(&k); + return res; } pub fn set_option(&self, k: String, v: String) { - self.lc.write().unwrap().set_option(k, v); + self.lc.write().unwrap().set_option(k.clone(), v); } - + #[inline] pub fn load_config(&self) -> PeerConfig { load_config(&self.id) @@ -54,7 +223,7 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn get_platform(&mut self, is_remote: bool) -> String { + pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() } else { @@ -62,7 +231,7 @@ impl Session { } } - pub fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + pub fn get_path_sep(&self, is_remote: bool) -> &'static str { let p = self.get_platform(is_remote); if &p == "Windows" { return "\\"; @@ -70,32 +239,161 @@ impl Session { 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(); + } + + 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 reconnect(&self) { + println!("reconnecting"); + 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_cursor_position(&self, cp: CursorPosition); fn set_display(&self, x: i32, y: i32, w: i32, h: i32); + fn set_peer_info( + &self, + username: &str, + hostname: &str, + platform: &str, + sas_enabled: bool, + displays: &Vec>, + version: &str, + current_display: usize, + is_file_transfer: bool, + ); // flutter fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); fn update_pi(&self, pi: PeerInfo); 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 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 add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ); fn update_transfer_list(&self); // fn update_folder_files(&self); // TODO - 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 job_progress(&self, id:i32, file_num:i32, speed:f64, finished_size:f64); + 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 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); } - impl Deref for Session { type Target = T; @@ -135,6 +433,7 @@ impl Interface for Session { 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)); + self.ui_handler.msgbox(msgtype, title, text, retry); } fn handle_login_error(&mut self, err: &str) -> bool { @@ -142,8 +441,15 @@ impl Interface for Session { } fn handle_peer_info(&mut self, pi: PeerInfo) { + let mut lc = self.lc.write().unwrap(); + // let mut pi_sciter = Value::map(); - let username = self.lc.read().unwrap().get_username(&pi); + let username = lc.get_username(&pi); + + // flutter + let mut displays = Vec::new(); + let mut current_index = pi.current_display as usize; + // pi_sciter.set_item("username", username.clone()); // pi_sciter.set_item("hostname", pi.hostname.clone()); // pi_sciter.set_item("platform", pi.platform.clone()); @@ -158,7 +464,7 @@ impl Interface for Session { } } else if !self.is_port_forward() { if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(username, pi); + lc.handle_peer_info(username, pi); self.update_privacy_mode(); self.msgbox("error", "Remote Error", "No Display"); return; @@ -173,13 +479,38 @@ impl Interface for Session { // displays.push(display); // } // pi_sciter.set_item("displays", displays); - let mut current = pi.current_display as usize; - if current >= pi.displays.len() { - current = 0; + + // flutter + 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_index >= pi.displays.len() { + current_index = 0; + } + + if current_index >= pi.displays.len() { + current_index = 0; } // pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current]; + let current = &pi.displays[current_index]; self.set_display(current.x, current.y, current.width, current.height); + + self.set_peer_info( + &username, + &pi.hostname, + &pi.platform, + pi.sas_enabled, + &displays, + &pi.version, + current_index, + lc.is_file_transfer, + ); + // 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. @@ -192,12 +523,12 @@ impl Interface for Session { // ); // log::info!("[video] initialized: {:?}", ok); // }); - let p = self.lc.read().unwrap().should_auto_login(); + let p = lc.should_auto_login(); if !p.is_empty() { input_os_password(p, true, self.clone()); } } - self.lc.write().unwrap().handle_peer_info(username, pi); + lc.handle_peer_info(username, pi); self.update_privacy_mode(); // self.update_pi(pi); if self.is_file_transfer() { @@ -255,3 +586,1385 @@ impl Interface for Session { self.lc.read().unwrap().force_relay } } + +#[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(); + } + 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; +} + +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); +} + +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); +const MILLI1: Duration = Duration::from_millis(1); + +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 + // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + 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 + // .call("jobError", &make_args!(id, err, file_num)); + self.handler.job_error(id, err, file_num); + } else { + // self.handler.call("jobDone", &make_args!(id, file_num)); + 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(); + 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!()); + 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.call( + // "addJob", + // &make_args!( + // cnt, + // job.to.clone(), + // job.remote.clone(), + // job.file_num, + // job.show_hidden, + // false + // ), + // ); + 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.call( + // "addJob", + // &make_args!( + // cnt, + // job.remote.clone(), + // job.to.clone(), + // job.file_num, + // job.show_hidden, + // true + // ), + // ); + 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.call("updateTransferList", &make_args!()); + 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.call( + // "confirmDeleteFiles", + // &make_args!(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.call( + // "jobProgress", + // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), + // ); + 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.call2("closeSuccess", &make_args!()); + self.handler.close_success(); + // self.handler.call("adaptSize", &make_args!()); + 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 { + 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 + // ), + // ); + 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.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // write_path, + // false + // ), + // ); + 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.call("newMessage", &make_args!(c.text)); // TODO + } + 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)); + self.handler.set_permission("keyboard", p.enabled); + } + Permission::Clipboard => { + SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + // self.handler + // .call2("setPermission", &make_args!("clipboard", p.enabled)); + self.handler.set_permission("clipboard", p.enabled); + } + Permission::Audio => { + // self.handler + // .call2("setPermission", &make_args!("audio", p.enabled)); + 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 + // .call2("setPermission", &make_args!("file", p.enabled)); + self.handler.set_permission("file", p.enabled); + } + Permission::Restart => { + // self.handler + // .call2("setPermission", &make_args!("restart", p.enabled)); + self.handler.set_permission("restart", p.enabled); + } + } + } + Some(misc::Union::SwitchDisplay(s)) => { + // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO + 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.call("updateBlockInputState", &make_args!(on)); // TODO + } + + 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", &[]); + 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, + } + } +} + +#[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); +} From ae265ca83619b15261690eeb5aa60bb3b519cd4f Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 31 Aug 2022 22:24:57 +0800 Subject: [PATCH 24/35] flutter.rs Session -> ui_session_interface.rs --- src/client.rs | 13 + src/flutter.rs | 610 ------------------------------------ src/ui/remote.rs | 114 +------ src/ui_session_interface.rs | 234 +++++++++++--- 4 files changed, 213 insertions(+), 758 deletions(-) diff --git a/src/client.rs b/src/client.rs index 64c7daf4d..ddb093b08 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; +use enigo::{Enigo, KeyboardControllable}; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -58,6 +59,18 @@ lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); +} + +pub 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) +} + cfg_if::cfg_if! { if #[cfg(target_os = "android")] { diff --git a/src/flutter.rs b/src/flutter.rs index 88c6f1961..514048e31 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -301,616 +301,6 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } -// #[derive(Clone)] -// pub struct Session { -// id: String, -// sender: Arc>>>, // UI to rust -// lc: Arc>, -// events2ui: Arc>>>, -// } - -// impl Session1 { -// /// 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 add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { -// // TODO check same id -// let session_id = get_session_id(id.to_owned()); -// LocalConfig::set_remote_id(&session_id); -// // TODO close -// // Self::close(); -// let session = Session { -// id: session_id.clone(), -// sender: Default::default(), -// lc: Default::default(), -// events2ui: Arc::new(RwLock::new(None)), -// }; -// session.lc.write().unwrap().initialize( -// session_id.clone(), -// is_file_transfer, -// is_port_forward, -// ); -// SESSIONS -// .write() -// .unwrap() -// .insert(id.to_owned(), session.clone()); -// Ok(()) -// } - -// /// Create a new remote 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 start(id: &str, events2ui: StreamSink) -> ResultType<()> { -// if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { -// *session.events2ui.write().unwrap() = Some(events2ui); -// let session = session.clone(); -// std::thread::spawn(move || { -// let is_file_transfer = session.lc.read().unwrap().is_file_transfer; -// let is_port_forward = session.lc.read().unwrap().is_port_forward; -// Connection::start(session, is_file_transfer, is_port_forward); -// }); -// Ok(()) -// } else { -// bail!("No session with peer id {}", id) -// } -// } - -// /// 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, 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)); -// } - -// /// 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); -// } - -// /// Push an event to the event queue. -// /// An event is stored as json in the event queue. -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the event. -// /// * `event` - Fields of the event content. -// fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { -// let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); -// assert!(h.get("name").is_none()); -// h.insert("name", name); -// let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); -// if let Some(stream) = &*self.events2ui.read().unwrap() { -// stream.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); -// } -// } - -// /// 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); -// } - -// /// 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); -// } - -// /// 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); -// } -// } -// } - -// /// 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 update_quality_status(&self, status: QualityStatus) { -// const NULL: String = String::new(); -// self.push_event( -// "update_quality_status", -// vec![ -// ("speed", &status.speed.map_or(NULL, |it| it)), -// ("fps", &status.fps.map_or(NULL, |it| it.to_string())), -// ("delay", &status.delay.map_or(NULL, |it| it.to_string())), -// ( -// "target_bitrate", -// &status.target_bitrate.map_or(NULL, |it| it.to_string()), -// ), -// ( -// "codec_format", -// &status.codec_format.map_or(NULL, |it| it.to_string()), -// ), -// ], -// ); -// } - -// pub 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)); -// } - -// 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)); -// } - -// fn on_error(&self, err: &str) { -// self.msgbox("error", "Error", err); -// } -// } - -// 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 { -// todo!() -// } - -// fn is_port_forward(&self) -> bool { -// todo!() -// } - -// fn is_rdp(&self) -> bool { -// todo!() -// } - -// fn msgbox(&self, msgtype: &str, title: &str, text: &str) { -// let has_retry = if check_if_retry(msgtype, title, text) { -// "true" -// } else { -// "" -// }; -// self.push_event( -// "msgbox", -// vec![ -// ("type", msgtype), -// ("title", title), -// ("text", text), -// ("hasRetry", has_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 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()); -// self.push_event( -// "peer_info", -// 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()), -// ], -// ); -// 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; -// } -// } -// } - // struct Connection { // video_handler: VideoHandler, // audio_handler: AudioHandler, diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 49812e09a..9e8c8fc51 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -55,18 +55,9 @@ 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(); } -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); @@ -1111,38 +1102,7 @@ impl SciterSession { 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.0); - // 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); - } - } - } - + // TODO 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() { @@ -1285,24 +1245,6 @@ impl SciterSession { "".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); - self.key_down_or_up(1, key_event, false, false, false, false); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.key_down_or_up(3, key_event, true, true, false, false); - } - } - - fn lock_screen(&mut self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - self.key_down_or_up(1, key_event, false, false, false, false); - } - fn transfer_file(&mut self) { let id = self.get_id(); let args = vec!["--file-transfer", &id, &self.password]; @@ -1319,60 +1261,6 @@ impl SciterSession { } } - fn key_down_or_up( - &mut self, - down_or_up: i32, - evt: KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let mut key_event = evt; - - 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) && common::valid_for_numlock(&key_event) { - key_event.modifiers.push(ControlKey::NumLock.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(Data::Message(msg_out)); - } - // #[inline] // fn set_cursor_id(&mut self, id: String) { // self.call("setCursorId", &make_args!(id)); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a8871fac1..03666ed92 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,7 +1,7 @@ use crate::client::{ - self, check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - load_config, start_video_audio_threads, Client, CodecFormat, FileManager, LoginConfigHandler, - MediaData, MediaSender, QualityStatus, SEC30, + self, check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, Client, CodecFormat, + FileManager, Key, LoginConfigHandler, MediaData, MediaSender, QualityStatus, KEY_MAP, SEC30, }; use crate::common::{ self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, @@ -9,6 +9,7 @@ use crate::common::{ use crate::platform; use crate::{client::Data, client::Interface}; use async_trait::async_trait; +use enigo::{Enigo, KeyboardControllable}; use hbb_common::config::{Config, LocalConfig, PeerConfig, TransferSerde}; use hbb_common::fs::{ can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, @@ -43,11 +44,11 @@ pub struct Session { impl Session { pub fn get_view_style(&self) -> String { - return self.lc.read().unwrap().view_style.clone(); + self.lc.read().unwrap().view_style.clone() } pub fn get_image_quality(&self) -> String { - return self.lc.read().unwrap().image_quality.clone(); + self.lc.read().unwrap().image_quality.clone() } pub fn save_view_style(&mut self, value: String) { @@ -65,8 +66,7 @@ impl Session { } pub fn get_toggle_option(&self, name: String) -> bool { - let res = self.lc.read().unwrap().get_toggle_option(&name); - return res; + self.lc.read().unwrap().get_toggle_option(&name) } pub fn is_privacy_mode_supported(&self) -> bool { @@ -196,12 +196,18 @@ impl Session { } pub fn get_option(&self, k: String) -> String { - let res = self.lc.read().unwrap().get_option(&k); - return res; + 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, v: String) { - self.lc.write().unwrap().set_option(k.clone(), v); + 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] @@ -223,6 +229,72 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } + pub fn ctrl_alt_del(&mut self) { + if self.peer_platform() == "Windows" { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::CtrlAltDel); + self.key_down_or_up(1, key_event, false, false, false, false); + } else { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::Delete); + self.key_down_or_up(3, key_event, true, true, false, false); + } + } + + pub fn key_down_or_up( + &self, + down_or_up: i32, + evt: KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let mut key_event = evt; + + 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) && common::valid_for_numlock(&key_event) { + key_event.modifiers.push(ControlKey::NumLock.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(Data::Message(msg_out)); + } + pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() @@ -277,8 +349,122 @@ impl Session { self.send(Data::Message(msg_out)); } + pub fn lock_screen(&mut self) { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + self.key_down_or_up(1, key_event, false, false, false, false); + } + + // 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); + } + } + } + + self.key_down_or_up(v, key_event, alt, ctrl, shift, command); + } + + pub 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); + } + } + } + pub fn reconnect(&self) { - println!("reconnecting"); + self.send(Data::Close); let cloned = self.clone(); let mut lock = self.thread.lock().unwrap(); lock.take().map(|t| t.join()); @@ -979,7 +1165,7 @@ impl Remote { Some(tx) } - async fn load_last_jobs(&mut self) { + fn load_last_jobs(&mut self) { log::info!("start load last jobs"); // self.handler.call("clearAllJobs", &make_args!()); self.handler.clear_all_jobs(); @@ -993,17 +1179,6 @@ impl Remote { 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 - // ), - // ); self.handler.add_job( cnt, job.to.clone(), @@ -1019,17 +1194,6 @@ impl Remote { 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 - // ), - // ); self.handler.add_job( cnt, job.remote.clone(), From 41a53e4983fe681442e3d8aee2cdfec91c91f3ec Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 09:48:53 +0800 Subject: [PATCH 25/35] refactor io_loop --- src/client.rs | 7 +- src/client/io_loop.rs | 1205 +++++++++++++++++++++++++++++++++ src/flutter.rs | 1176 ++------------------------------ src/ui/remote.rs | 435 +----------- src/ui_session_interface.rs | 1258 +---------------------------------- 5 files changed, 1326 insertions(+), 2755 deletions(-) create mode 100644 src/client/io_loop.rs diff --git a/src/client.rs b/src/client.rs index ddb093b08..6346af6d0 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::{mpsc, Arc, Mutex, RwLock, atomic::AtomicBool}, }; pub use async_trait::async_trait; @@ -48,7 +48,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. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs new file mode 100644 index 000000000..f7f8f4f18 --- /dev/null +++ b/src/client/io_loop.rs @@ -0,0 +1,1205 @@ +use crate::client::{ + Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, + SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, +}; +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(); + 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) + } + + 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 514048e31..0b8c3626f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,57 +1,36 @@ use std::{ collections::HashMap, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, + Arc, RwLock, }, }; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ - allow_err, bail, - compress::decompress, - config::{Config, LocalConfig, PeerConfig, TransferSerde}, - fs::{ - self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, - transform_windows_path, DigestCheckResult, - }, - log, + bail, + config::{LocalConfig}, message_proto::*, - protobuf::Message as _, - rendezvous_proto::ConnType, - tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, - }, - ResultType, Stream, + ResultType, }; use crate::{ - common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}, 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 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"; -const MILLI1: Duration = Duration::from_millis(1); - lazy_static::lazy_static! { pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } -static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); - #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, @@ -115,7 +94,7 @@ impl InvokeUi for FlutterHandler { } fn set_permission(&self, name: &str, value: bool) { - // todo!() + self.push_event("permission", vec![(name, &value.to_string())]); } fn update_pi(&self, pi: PeerInfo) { @@ -157,11 +136,14 @@ impl InvokeUi for FlutterHandler { } fn job_error(&self, id: i32, err: String, file_num: i32) { - // todo!() + self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); } fn job_done(&self, id: i32, file_num: i32) { - // todo!() + self.push_event( + "job_done", + vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], + ); } fn clear_all_jobs(&self) { @@ -189,11 +171,27 @@ impl InvokeUi for FlutterHandler { } fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { - // todo!() + 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) { - // todo!() + 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) { @@ -245,6 +243,35 @@ impl InvokeUi for FlutterHandler { ], ); } + + fn new_message(&self, msg: String) { + self.push_event("chat_client_mode", vec![("text", &msg)]); + } + + fn switch_display(&self, display: &SwitchDisplay) { + self.push_event( + "switch_display", + vec![ + ("display", &display.to_string()), + ("x", &display.x.to_string()), + ("y", &display.y.to_string()), + ("width", &display.width.to_string()), + ("height", &display.height.to_string()), + ], + ); + } + + fn update_block_input_state(&self, on: bool) { + self.push_event( + "update_block_input_state", + [("input_state", if on { "on" } else { "off" })].into(), + ); + } + + #[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. @@ -290,6 +317,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { + // TODO is_file_transfer is_port_forward // let is_file_transfer = session.lc.read().unwrap().is_file_transfer; // let is_port_forward = session.lc.read().unwrap().is_port_forward; // Connection::start(session, is_file_transfer, is_port_forward); @@ -301,1091 +329,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } -// 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. -// /// * `is_port_forward` - Whether the connection is for port forward. -// #[tokio::main(flavor = "current_thread")] -// async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { -// let mut last_recv_time = Instant::now(); -// let (sender, mut receiver) = mpsc::unbounded_channel::(); -// let mut stop_clipboard = None; -// if !is_file_transfer && !is_port_forward { -// stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); -// } -// *session.sender.write().unwrap() = Some(sender.clone()); -// let conn_type = if is_file_transfer { -// session.lc.write().unwrap().is_file_transfer = true; -// ConnType::FILE_TRANSFER -// } else if is_port_forward { -// ConnType::PORT_FORWARD // TODO: RDP -// } else { -// ConnType::DEFAULT_CONN -// }; -// let key = Config::get_option("key"); -// let token = Config::get_option("access_token"); - -// // TODO rdp & cli args -// let is_rdp = false; -// let args: Vec = Vec::new(); - -// if is_port_forward { -// if 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 args.len() == 0 { -// let pfs = session.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 = session.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 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, -// }; - -// 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) { -// if let Some(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( -// "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"); -// } -// _ => {} -// } -// } - -// #[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()); -// } - -// 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 -// } -// } - // 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::{ diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9e8c8fc51..2d412ea9b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,8 +2,8 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, + atomic::{AtomicBool, Ordering}, + Arc, Mutex, }, }; @@ -22,35 +22,15 @@ 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 enigo::{self}; +use hbb_common::{allow_err, log, message_proto::*}; #[cfg(windows)] use crate::clipboard_file::*; use crate::{ client::*, - common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, - ui_session_interface::{io_loop, InvokeUi, Remote, Session, SERVER_KEYBOARD_ENABLED}, + ui_session_interface::{InvokeUi, Session}, }; -use errno; type Video = AssetPtr; @@ -131,7 +111,7 @@ impl InvokeUi for SciterHandler { self.call2("setPermission", &make_args!(name, value)); } - fn update_pi(&self, pi: PeerInfo) {} + fn update_pi(&self, pi: PeerInfo) {} // TODO dup flutter fn close_success(&self) { self.call2("closeSuccess", &make_args!()); @@ -165,15 +145,15 @@ impl InvokeUi for SciterHandler { } fn job_error(&self, id: i32, err: String, file_num: i32) { - todo!() + self.call("jobError", &make_args!(id, err, file_num)); } fn job_done(&self, id: i32, file_num: i32) { - todo!() + self.call("jobDone", &make_args!(id, file_num)); } fn clear_all_jobs(&self) { - todo!() + self.call("clearAllJobs", &make_args!()); } fn add_job( @@ -189,19 +169,25 @@ impl InvokeUi for SciterHandler { } fn update_transfer_list(&self) { - todo!() + self.call("updateTransferList", &make_args!()); } fn confirm_delete_files(&self, id: i32, i: i32, name: String) { - todo!() + self.call("confirmDeleteFiles", &make_args!(id, i, name)); } fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { - todo!() + 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) { - todo!() + self.call( + "jobProgress", + &make_args!(id, file_num, speed, finished_size), + ); } fn adapt_size(&self) { @@ -227,11 +213,22 @@ impl InvokeUi for SciterHandler { current_display: usize, is_file_transfer: bool, ) { - todo!() } fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { - todo!() + 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)); } } @@ -405,21 +402,7 @@ impl SciterSession { Self(session) } - // 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()) - // ), - // ); - // } - + // TODO fn start_keyboard_hook(&'static self) { if self.is_port_forward() || self.is_file_transfer() { return; @@ -652,14 +635,6 @@ impl SciterSession { }); } - // 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(); - // } - // TODO fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); @@ -669,87 +644,6 @@ impl SciterSession { 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; - // } - // TODO fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] @@ -775,51 +669,6 @@ impl SciterSession { } } - // 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() - // } - // TODO fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { let size = (x, y, w, h); @@ -881,34 +730,6 @@ impl SciterSession { 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 @@ -925,10 +746,6 @@ impl SciterSession { 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(); @@ -938,158 +755,11 @@ impl SciterSession { 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()); - // } - // close_state sciter only fn save_close_state(&mut self, k: String, v: String) { self.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.thread.lock().unwrap(); - // lock.take().map(|t| t.join()); - // *lock = 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 enter(&mut self) { #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(true); @@ -1102,30 +772,6 @@ impl SciterSession { IS_IN.store(false, Ordering::SeqCst); } - // TODO - 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 { @@ -1260,21 +906,6 @@ impl SciterSession { log::error!("Failed to spawn IP tunneling: {}", err); } } - - // #[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 set_display(&self, x: i32, y: i32, w: i32, h: i32) { - // self.call("setDisplay", &make_args!(x, y, w, h)); - // } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 03666ed92..a164a2d94 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,33 +1,23 @@ +use crate::client::io_loop::Remote; use crate::client::{ - self, check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, Client, CodecFormat, - FileManager, Key, LoginConfigHandler, MediaData, MediaSender, QualityStatus, KEY_MAP, SEC30, + check_if_retry, get_key_state, 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, }; -use crate::common::{ - self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, -}; -use crate::platform; +use crate::common; + use crate::{client::Data, client::Interface}; use async_trait::async_trait; -use enigo::{Enigo, KeyboardControllable}; -use hbb_common::config::{Config, LocalConfig, 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::*, sleep}; + +use hbb_common::config::{Config, LocalConfig, PeerConfig}; + +use hbb_common::tokio::{self, sync::mpsc}; + +use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; #[derive(Clone, Default)] @@ -541,6 +531,7 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { 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, username: &str, @@ -570,14 +561,18 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { show_hidden: bool, is_remote: bool, ); + fn new_message(&self, msg: String); fn update_transfer_list(&self); - // fn update_folder_files(&self); // TODO + // 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 { @@ -604,21 +599,23 @@ impl Interface for Session { } } + // TODO flutter fn is_file_transfer(&self) -> bool { self.cmd == "--file-transfer" } + // TODO flutter fn is_port_forward(&self) -> bool { self.cmd == "--port-forward" || self.is_rdp() } + // TODO flutter fn is_rdp(&self) -> bool { self.cmd == "--rdp" } 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)); self.ui_handler.msgbox(msgtype, title, text, retry); } @@ -916,1217 +913,6 @@ async fn start_one_port_forward( log::info!("port forward (:{}) exit", port); } -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); -const MILLI1: Duration = Duration::from_millis(1); - -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 - // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); - 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 - // .call("jobError", &make_args!(id, err, file_num)); - self.handler.job_error(id, err, file_num); - } else { - // self.handler.call("jobDone", &make_args!(id, file_num)); - 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(); - 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) - } - - fn load_last_jobs(&mut self) { - log::info!("start load last jobs"); - // self.handler.call("clearAllJobs", &make_args!()); - 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.call("updateTransferList", &make_args!()); - 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.call( - // "confirmDeleteFiles", - // &make_args!(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.call( - // "jobProgress", - // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - // ); - 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.call2("closeSuccess", &make_args!()); - self.handler.close_success(); - // self.handler.call("adaptSize", &make_args!()); - 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 { - 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 - // ), - // ); - 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.call( - // "overrideFileConfirm", - // &make_args!( - // digest.id, - // digest.file_num, - // write_path, - // false - // ), - // ); - 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.call("newMessage", &make_args!(c.text)); // TODO - } - 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)); - self.handler.set_permission("keyboard", p.enabled); - } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - // self.handler - // .call2("setPermission", &make_args!("clipboard", p.enabled)); - self.handler.set_permission("clipboard", p.enabled); - } - Permission::Audio => { - // self.handler - // .call2("setPermission", &make_args!("audio", p.enabled)); - 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 - // .call2("setPermission", &make_args!("file", p.enabled)); - self.handler.set_permission("file", p.enabled); - } - Permission::Restart => { - // self.handler - // .call2("setPermission", &make_args!("restart", p.enabled)); - self.handler.set_permission("restart", p.enabled); - } - } - } - Some(misc::Union::SwitchDisplay(s)) => { - // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO - 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.call("updateBlockInputState", &make_args!(on)); // TODO - } - - 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", &[]); - 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, - } - } -} - #[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 }); From 2891c1b148e38a639f97207ffdcc0526e3463a0d Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 16:21:41 +0800 Subject: [PATCH 26/35] refactor set_peer_info --- flutter/lib/models/model.dart | 1 + src/client.rs | 4 +- src/flutter.rs | 57 ++++++++------------ src/ui/remote.rs | 36 ++++++++----- src/ui_session_interface.rs | 97 +++++------------------------------ 5 files changed, 59 insertions(+), 136 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 171a41dfa..c01451280 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -325,6 +325,7 @@ class FfiModel with ChangeNotifier { } if (evt['is_file_transfer'] == "true") { + // TODO is file transfer parent.target?.fileModel.onReady(); } else { _pi.displays = []; diff --git a/src/client.rs b/src/client.rs index 6346af6d0..184170e4a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1278,13 +1278,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(); let serde = PeerInfoSerde { - username, + username: pi.username.clone(), hostname: pi.hostname.clone(), platform: pi.platform.clone(), }; diff --git a/src/flutter.rs b/src/flutter.rs index 0b8c3626f..1244e521a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,24 +1,13 @@ use std::{ collections::HashMap, - sync::{ - Arc, RwLock, - }, + sync::{Arc, RwLock}, }; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{ - bail, - config::{LocalConfig}, - message_proto::*, - ResultType, -}; - -use crate::{ - ui_session_interface::{io_loop, InvokeUi, Session}, -}; - +use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType}; +use crate::ui_session_interface::{io_loop, InvokeUi, Session}; use crate::{client::*, flutter_ffi::EventToUI}; @@ -97,10 +86,6 @@ impl InvokeUi for FlutterHandler { self.push_event("permission", vec![(name, &value.to_string())]); } - fn update_pi(&self, pi: PeerInfo) { - // todo!() - } - fn close_success(&self) { // todo!() } @@ -204,29 +189,27 @@ impl InvokeUi for FlutterHandler { } } - fn set_peer_info( - &self, - username: &str, - hostname: &str, - platform: &str, - sas_enabled: bool, - displays: &Vec>, - version: &str, - current_display: usize, - is_file_transfer: bool, - ) { - let displays = serde_json::ser::to_string(displays).unwrap_or("".to_owned()); + 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", username), - ("hostname", hostname), - ("platform", platform), - ("sas_enabled", &sas_enabled.to_string()), + ("username", &pi.username), + ("hostname", &pi.hostname), + ("platform", &pi.platform), + ("sas_enabled", &pi.sas_enabled.to_string()), ("displays", &displays), - ("version", &version), - ("current_display", ¤t_display.to_string()), - ("is_file_transfer", &is_file_transfer.to_string()), + ("version", &pi.version), + ("current_display", &pi.current_display.to_string()), ], ); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 2d412ea9b..4310f64bf 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -96,6 +96,10 @@ impl InvokeUi for SciterHandler { 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); @@ -111,8 +115,6 @@ impl InvokeUi for SciterHandler { self.call2("setPermission", &make_args!(name, value)); } - fn update_pi(&self, pi: PeerInfo) {} // TODO dup flutter - fn close_success(&self) { self.call2("closeSuccess", &make_args!()); } @@ -202,17 +204,25 @@ impl InvokeUi for SciterHandler { .map(|v| v.render_frame(data).ok()); } - fn set_peer_info( - &self, - username: &str, - hostname: &str, - platform: &str, - sas_enabled: bool, - displays: &Vec>, - version: &str, - current_display: usize, - is_file_transfer: bool, - ) { + 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) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a164a2d94..2f401e26f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -532,20 +532,9 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { 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, - username: &str, - hostname: &str, - platform: &str, - sas_enabled: bool, - displays: &Vec>, - version: &str, - current_display: usize, - is_file_transfer: bool, - ); // flutter + fn set_peer_info(&self, peer_info: &PeerInfo); // flutter fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); - fn update_pi(&self, pi: PeerInfo); fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); fn set_connection_type(&self, is_secured: bool, direct: bool); @@ -623,20 +612,11 @@ impl Interface for Session { self.lc.write().unwrap().handle_login_error(err, self) } - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut lc = self.lc.write().unwrap(); - - // let mut pi_sciter = Value::map(); - let username = lc.get_username(&pi); - - // flutter - let mut displays = Vec::new(); - let mut current_index = pi.current_display as usize; - - // 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); + 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); } @@ -647,73 +627,22 @@ impl Interface for Session { } } else if !self.is_port_forward() { if pi.displays.is_empty() { - lc.handle_peer_info(username, pi); + self.lc.write().unwrap().handle_peer_info(pi); self.update_privacy_mode(); 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); - - // flutter - 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_index >= pi.displays.len() { - current_index = 0; - } - - if current_index >= pi.displays.len() { - current_index = 0; - } - // pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current_index]; - self.set_display(current.x, current.y, current.width, current.height); - - self.set_peer_info( - &username, - &pi.hostname, - &pi.platform, - pi.sas_enabled, - &displays, - &pi.version, - current_index, - lc.is_file_transfer, - ); - - // 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 = lc.should_auto_login(); + 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); } - lc.handle_peer_info(username, pi); self.update_privacy_mode(); - // self.update_pi(pi); + self.set_peer_info(&pi); + self.lc.write().unwrap().handle_peer_info(pi); + if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { From ee839875234505ce29197d1eeee4825886dd4fad Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 17:36:37 +0800 Subject: [PATCH 27/35] sciter input & conn_type and other InvokeUi impl --- src/client.rs | 32 ++--- src/flutter.rs | 43 +++--- src/flutter_ffi.rs | 15 +- src/ui/remote.rs | 265 +++--------------------------------- src/ui_session_interface.rs | 263 +++++++++++++++++++++++++++++++++-- 5 files changed, 312 insertions(+), 306 deletions(-) diff --git a/src/client.rs b/src/client.rs index 184170e4a..8f6cb12bb 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, atomic::AtomicBool}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -864,8 +864,7 @@ impl VideoHandler { #[derive(Default)] pub struct LoginConfigHandler { id: String, - pub is_file_transfer: bool, - pub is_port_forward: bool, + pub conn_type: ConnType, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, @@ -904,12 +903,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; @@ -1066,7 +1063,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; @@ -1112,7 +1110,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; @@ -1348,19 +1347,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 diff --git a/src/flutter.rs b/src/flutter.rs index 1244e521a..b84e91ce8 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,7 +5,7 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType}; +use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType}; use crate::ui_session_interface::{io_loop, InvokeUi, Session}; @@ -74,9 +74,8 @@ impl InvokeUi for FlutterHandler { ); } - fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - // todo!() - } + /// 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()); @@ -86,9 +85,7 @@ impl InvokeUi for FlutterHandler { self.push_event("permission", vec![(name, &value.to_string())]); } - fn close_success(&self) { - // todo!() - } + fn close_success(&self) {} fn update_quality_status(&self, status: QualityStatus) { const NULL: String = String::new(); @@ -179,9 +176,7 @@ impl InvokeUi for FlutterHandler { ); } - fn adapt_size(&self) { - // todo!() - } + fn adapt_size(&self) {} fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { @@ -265,27 +260,37 @@ impl InvokeUi for FlutterHandler { /// * `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<()> { - // TODO check same id let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); - // TODO close - // Self::close(); - // TODO cmd passwd args 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.clone(), is_file_transfer, is_port_forward); - SESSIONS + .initialize(session_id, conn_type); + + if let Some(same_id_session) = SESSIONS .write() .unwrap() - .insert(id.to_owned(), session.clone()); + .insert(id.to_owned(), session) + { + same_id_session.close(); + } + Ok(()) } @@ -300,10 +305,6 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - // TODO is_file_transfer is_port_forward - // let is_file_transfer = session.lc.read().unwrap().is_file_transfer; - // let is_port_forward = session.lc.read().unwrap().is_port_forward; - // Connection::start(session, is_file_transfer, is_port_forward); io_loop(session); }); Ok(()) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5226416b2..69da5f540 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -190,20 +190,20 @@ pub fn session_toggle_option(id: String, value: String) { } pub fn session_set_image_quality(id: String, value: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.set_image_quality(value); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_image_quality(value); } } pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.lock_screen(); + session.lock_screen(); } } pub fn session_ctrl_alt_del(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.ctrl_alt_del(); + session.ctrl_alt_del(); } } @@ -224,13 +224,13 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.input_key(&name, down, press, alt, ctrl, shift, command); + session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.input_string(&value); + session.input_string(&value); } } @@ -686,7 +686,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(); @@ -719,7 +718,7 @@ pub fn session_send_mouse(id: String, msg: String) { } << 3; } if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.send_mouse(mask, x, y, alt, ctrl, shift, command); + session.send_mouse(mask, x, y, alt, ctrl, shift, command); } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4310f64bf..7e2c5cd9c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -22,14 +22,14 @@ use clipboard::{ cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context, get_rx_clip_client, server_clip_file, }; -use enigo::{self}; -use hbb_common::{allow_err, log, message_proto::*}; + +use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; #[cfg(windows)] use crate::clipboard_file::*; use crate::{ client::*, - ui_session_interface::{InvokeUi, Session}, + ui_session_interface::{InvokeUi, Session, IS_IN}, }; type Video = AssetPtr; @@ -38,9 +38,6 @@ lazy_static::lazy_static! { static ref VIDEO: Arc>> = Default::default(); } -static IS_IN: AtomicBool = AtomicBool::new(false); -static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); - #[cfg(windows)] static mut IS_ALT_GR: bool = false; @@ -397,255 +394,28 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let session: Session = Session { - cmd, + cmd: cmd.clone(), id: id.clone(), password: password.clone(), args, ..Default::default() }; - session.lc.write().unwrap().initialize( - id, - session.is_file_transfer(), - session.is_port_forward(), - ); + + 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 + }; + + session.lc.write().unwrap().initialize(id, conn_type); Self(session) } - // TODO - fn start_keyboard_hook(&'static self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - let me = self.clone(); - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - #[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"); // pass to rdev - use rdev::{EventType::*, *}; - 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) => (k, 1), - KeyRelease(k) => (k, 0), - _ => return, - }; - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control); - unsafe { - if IS_ALT_GR { - if alt || key == Key::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control); - let shift = get_key_state(enigo::Key::Shift); - #[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 { - Key::Alt => Some(ControlKey::Alt), - Key::AltGr => Some(ControlKey::RAlt), - Key::Backspace => Some(ControlKey::Backspace), - Key::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) - } - Key::ControlRight => Some(ControlKey::RControl), - Key::DownArrow => Some(ControlKey::DownArrow), - Key::Escape => Some(ControlKey::Escape), - Key::F1 => Some(ControlKey::F1), - Key::F10 => Some(ControlKey::F10), - Key::F11 => Some(ControlKey::F11), - Key::F12 => Some(ControlKey::F12), - Key::F2 => Some(ControlKey::F2), - Key::F3 => Some(ControlKey::F3), - Key::F4 => Some(ControlKey::F4), - Key::F5 => Some(ControlKey::F5), - Key::F6 => Some(ControlKey::F6), - Key::F7 => Some(ControlKey::F7), - Key::F8 => Some(ControlKey::F8), - Key::F9 => Some(ControlKey::F9), - Key::LeftArrow => Some(ControlKey::LeftArrow), - Key::MetaLeft => Some(ControlKey::Meta), - Key::MetaRight => Some(ControlKey::RWin), - Key::Return => Some(ControlKey::Return), - Key::RightArrow => Some(ControlKey::RightArrow), - Key::ShiftLeft => Some(ControlKey::Shift), - Key::ShiftRight => Some(ControlKey::RShift), - Key::Space => Some(ControlKey::Space), - Key::Tab => Some(ControlKey::Tab), - Key::UpArrow => Some(ControlKey::UpArrow), - Key::Delete => { - if is_win && ctrl && alt { - // me.ctrl_alt_del(); // TODO - return; - } - Some(ControlKey::Delete) - } - Key::Apps => Some(ControlKey::Apps), - Key::Cancel => Some(ControlKey::Cancel), - Key::Clear => Some(ControlKey::Clear), - Key::Kana => Some(ControlKey::Kana), - Key::Hangul => Some(ControlKey::Hangul), - Key::Junja => Some(ControlKey::Junja), - Key::Final => Some(ControlKey::Final), - Key::Hanja => Some(ControlKey::Hanja), - Key::Hanji => Some(ControlKey::Hanja), - Key::Convert => Some(ControlKey::Convert), - Key::Print => Some(ControlKey::Print), - Key::Select => Some(ControlKey::Select), - Key::Execute => Some(ControlKey::Execute), - Key::PrintScreen => Some(ControlKey::Snapshot), - Key::Help => Some(ControlKey::Help), - Key::Sleep => Some(ControlKey::Sleep), - Key::Separator => Some(ControlKey::Separator), - Key::KpReturn => Some(ControlKey::NumpadEnter), - Key::Kp0 => Some(ControlKey::Numpad0), - Key::Kp1 => Some(ControlKey::Numpad1), - Key::Kp2 => Some(ControlKey::Numpad2), - Key::Kp3 => Some(ControlKey::Numpad3), - Key::Kp4 => Some(ControlKey::Numpad4), - Key::Kp5 => Some(ControlKey::Numpad5), - Key::Kp6 => Some(ControlKey::Numpad6), - Key::Kp7 => Some(ControlKey::Numpad7), - Key::Kp8 => Some(ControlKey::Numpad8), - Key::Kp9 => Some(ControlKey::Numpad9), - Key::KpDivide => Some(ControlKey::Divide), - Key::KpMultiply => Some(ControlKey::Multiply), - Key::KpDecimal => Some(ControlKey::Decimal), - Key::KpMinus => Some(ControlKey::Subtract), - Key::KpPlus => Some(ControlKey::Add), - Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return; - } - Key::Home => Some(ControlKey::Home), - Key::End => Some(ControlKey::End), - Key::Insert => Some(ControlKey::Insert), - Key::PageUp => Some(ControlKey::PageUp), - Key::PageDown => Some(ControlKey::PageDown), - Key::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 { - Key::Num1 => '1', - Key::Num2 => '2', - Key::Num3 => '3', - Key::Num4 => '4', - Key::Num5 => '5', - Key::Num6 => '6', - Key::Num7 => '7', - Key::Num8 => '8', - Key::Num9 => '9', - Key::Num0 => '0', - Key::KeyA => 'a', - Key::KeyB => 'b', - Key::KeyC => 'c', - Key::KeyD => 'd', - Key::KeyE => 'e', - Key::KeyF => 'f', - Key::KeyG => 'g', - Key::KeyH => 'h', - Key::KeyI => 'i', - Key::KeyJ => 'j', - Key::KeyK => 'k', - Key::KeyL => 'l', - Key::KeyM => 'm', - Key::KeyN => 'n', - Key::KeyO => 'o', - Key::KeyP => 'p', - Key::KeyQ => 'q', - Key::KeyR => 'r', - Key::KeyS => 's', - Key::KeyT => 't', - Key::KeyU => 'u', - Key::KeyV => 'v', - Key::KeyW => 'w', - Key::KeyX => 'x', - Key::KeyY => 'y', - Key::KeyZ => 'z', - Key::Comma => ',', - Key::Dot => '.', - Key::SemiColon => ';', - Key::Quote => '\'', - Key::LeftBracket => '[', - Key::RightBracket => ']', - Key::BackSlash => '\\', - Key::Minus => '-', - Key::Equal => '=', - Key::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - // me.lock_screen(); // TODO - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - // me.key_down_or_up(down, key_event, alt, ctrl, shift, command); // TODO - }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); - } - - // TODO fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { @@ -654,7 +424,6 @@ impl SciterSession { v } - // TODO fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { @@ -679,7 +448,6 @@ impl SciterSession { } } - // TODO 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(); @@ -765,7 +533,6 @@ impl SciterSession { pi } - // close_state sciter only fn save_close_state(&mut self, k: String, v: String) { self.close_state.insert(k, v); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f401e26f..c08cc09ce 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -2,7 +2,7 @@ use crate::client::io_loop::Remote; use crate::client::{ check_if_retry, get_key_state, 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, + LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; use crate::common; @@ -11,15 +11,20 @@ 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 hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool}; 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); + #[derive(Clone, Default)] pub struct Session { pub cmd: String, @@ -219,7 +224,7 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn ctrl_alt_del(&mut self) { + pub fn ctrl_alt_del(&self) { if self.peer_platform() == "Windows" { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::CtrlAltDel); @@ -339,7 +344,7 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn lock_screen(&mut self) { + pub fn lock_screen(&self) { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); self.key_down_or_up(1, key_event, false, false, false, false); @@ -422,7 +427,7 @@ impl Session { } pub fn send_mouse( - &mut self, + &self, mask: i32, x: i32, y: i32, @@ -588,19 +593,16 @@ impl Interface for Session { } } - // TODO flutter fn is_file_transfer(&self) -> bool { - self.cmd == "--file-transfer" + self.lc.read().unwrap().conn_type.eq(&ConnType::FILE_TRANSFER) } - // TODO flutter fn is_port_forward(&self) -> bool { - self.cmd == "--port-forward" || self.is_rdp() + self.lc.read().unwrap().conn_type.eq(&ConnType::PORT_FORWARD) } - // TODO flutter fn is_rdp(&self) -> bool { - self.cmd == "--rdp" + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) } fn msgbox(&self, msgtype: &str, title: &str, text: &str) { @@ -658,7 +660,8 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // self.start_keyboard_hook(); // TODO + // TODO use event callbcak + self.start_keyboard_hook(); } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { @@ -699,6 +702,242 @@ impl Interface for Session { } } +// TODO use event callbcak +// sciter only +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 me = self.clone(); + let peer = self.peer_platform(); + let is_win = peer == "Windows"; + #[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"); // pass to rdev + use rdev::{EventType::*, *}; + 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) => (k, 1), + KeyRelease(k) => (k, 0), + _ => return, + }; + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control); + let shift = get_key_state(enigo::Key::Shift); + #[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 { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::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) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + me.ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::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 { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + me.lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", evt); + return; + } + } + me.key_down_or_up(down, key_event, alt, ctrl, shift, command); // TODO + }; + 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::(); From a435fc999a49ee99085f3607fa7c1db2b5eb4617 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 18:23:06 +0800 Subject: [PATCH 28/35] mobile build --- src/client.rs | 9 +++++---- src/client/io_loop.rs | 3 +++ src/ui_session_interface.rs | 13 +++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8f6cb12bb..25061bcfe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,7 +11,6 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; -use enigo::{Enigo, KeyboardControllable}; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -38,7 +37,6 @@ use hbb_common::{ }; pub use helper::LatencyController; pub use helper::*; -use scrap::Image; use scrap::{ codec::{Decoder, DecoderCfg}, VpxDecoderConfig, VpxVideoCodecId, @@ -61,14 +59,17 @@ 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(); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { - static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); + 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; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f7f8f4f18..e61690c32 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,6 +2,7 @@ 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}; @@ -235,6 +236,7 @@ impl Remote { 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 @@ -266,6 +268,7 @@ impl Remote { Some(tx) } + // TODO fn load_last_jobs(&mut self) { log::info!("start load last jobs"); self.handler.clear_all_jobs(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c08cc09ce..d89ce2d3b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,11 +1,12 @@ use crate::client::io_loop::Remote; use crate::client::{ - check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, + 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, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::client::get_key_state; use crate::common; - use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -129,6 +130,7 @@ impl Session { 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) } @@ -271,9 +273,11 @@ impl Session { { key_event.modifiers.push(ControlKey::Meta.into()); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.peer_platform() != "Mac OS" { if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { key_event.modifiers.push(ControlKey::NumLock.into()); @@ -318,6 +322,7 @@ impl Session { return "".to_owned(); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_icon(&self) -> String { crate::get_icon() } @@ -661,6 +666,7 @@ impl Interface for Session { } } // TODO use event callbcak + #[cfg(not(any(target_os = "android", target_os = "ios")))] self.start_keyboard_hook(); } @@ -704,6 +710,7 @@ impl Interface for Session { // 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() { @@ -948,6 +955,7 @@ pub async fn io_loop(handler: Session) { 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 @@ -1053,6 +1061,7 @@ pub async fn io_loop(handler: Session) { 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, From 59f0ffa82fafb7f8ace87b1a5503f2ec2cdd418d Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 16:41:05 +0800 Subject: [PATCH 29/35] flutter_desktop: menu bar, switch menu & shrink-stretch -> adaptive Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 38 ++++++++++--------- .../lib/desktop/widgets/remote_menubar.dart | 7 ++-- flutter/lib/models/model.dart | 38 +++---------------- flutter/lib/models/native_model.dart | 22 +++++------ src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/vn.rs | 2 + 25 files changed, 81 insertions(+), 66 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 3d5fdf7f6..6dbe4f8cd 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -315,29 +315,31 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, - child: Obx( - () => SwitchListTile( - value: curOption.value, - onChanged: (v) { - setOption(v); - }, - title: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: Text( + 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), - )), - dense: true, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, - ), - contentPadding: const EdgeInsets.only(left: 8.0), - ), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Obx(() => Switch( + value: curOption.value, + onChanged: (v) => setOption(v), + )), + )) + ])), + onPressed: () { + setOption(!curOption.value); + }, ), ) ]; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0e931dd71..2e8c7fb63 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -406,14 +406,13 @@ class _RemoteMenubarState extends State { MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ - Tuple2(translate('Original'), 'original'), - Tuple2(translate('Shrink'), 'shrink'), - Tuple2(translate('Stretch'), 'stretch'), + Tuple2(translate('Scale original'), 'original'), + Tuple2(translate('Scale adaptive'), 'adaptive'), ], curOptionGetter: () async { return await bind.sessionGetOption( id: widget.id, arg: 'view-style') ?? - ''; + 'adaptive'; }, optionSetter: (String v) async { await bind.sessionPeerOption( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c01451280..da06b7fb9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -497,39 +497,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 / (parent.target?.ffiModel.display.width ?? 720); + final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + _scale = s1 < s2 ? s1 : s2; } _x = (size.width - getDisplayWidth() * _scale) / 2; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 57372cdb9..2a8391723 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); } } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index fdb23f88e..c6efaee85 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 d9ddf78cc..99d0ae694 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 0e2d99425..883ef27a5 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 20cd9330e..649199f0d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 fe12e2d24..34b89c350 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 313ea8cac..82395dd7a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 c8b12243f..00cfe3a3c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 a6356b000..d1a259119 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 8548eb6bc..3447d3388 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 fdf8d27d9..65cdd4b3d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -316,5 +316,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Collegamento a relè"), ("Secure Connection", "Connessione sicura"), ("Insecure Connection", "Connessione insicura"), + ("Scale original", "Scala originale"), + ("Scale adaptive", "Scala adattiva"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 1d031f2f2..cbe31bf9f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 19d4c7ddf..44ba589f6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 251c349a2..42bd49bbb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -318,5 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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.rs b/src/lang/pt_PT.rs index fd4384767..e8c62d78a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 85eda60e6..cdd4128b5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 def560217..5bbdd846d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 4c04618aa..b92d0aca6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 081b7bf55..8cf46a196 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 9738ed469..c5ec537b4 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 46276dd2a..836b5ad12 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 474e57337..ba9e4cb86 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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(); } From 4b9805b0f3211569680cab2f9b23e1966f237c89 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 18:41:55 +0800 Subject: [PATCH 30/35] flutter_desktop: custom image quality Signed-off-by: fufesou --- flutter/lib/common.dart | 86 +++++++------ flutter/lib/consts.dart | 7 +- .../desktop/pages/connection_tab_page.dart | 3 +- flutter/lib/desktop/pages/remote_page.dart | 15 +-- flutter/lib/desktop/widgets/popup_menu.dart | 116 ++++++++++++------ .../lib/desktop/widgets/remote_menubar.dart | 89 ++++++++++---- flutter/lib/models/model.dart | 15 ++- src/flutter_ffi.rs | 30 +++-- 8 files changed, 240 insertions(+), 121 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6ba917bf3..e8632caaa 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, + getMsgBoxButton(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, + getMsgBoxButton(translate('Cancel'), () { + dialogManager.dismissAll(); + })); + } + // TODO: test this button + if (type.contains("hasclose")) { + buttons.insert( + 0, + getMsgBoxButton(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)), + actions: buttons)); +} + +Widget getMsgBoxButton(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,44 +477,14 @@ void msgBox( onPressed: onPressed, child: Text(translate(text), style: TextStyle(color: MyTheme.accent)))); +} +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(); - // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (type.indexOf("custom") < 0) { - 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)), + content: content, actions: buttons)); } @@ -495,13 +503,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"; } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 6c67e2ab9..95a6faaa2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -8,7 +8,10 @@ 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_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 4175bd11b..5687c5c7e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -61,6 +61,7 @@ class _ConnectionTabPageState extends State { final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); + ConnectionTypeState.init(id); tabController.add(TabInfo( key: id, label: id, @@ -108,7 +109,7 @@ class _ConnectionTabPageState extends State { }, tabBuilder: (key, icon, label, themeConf) => Obx(() { final connectionType = ConnectionTypeState.find(key); - if (!ConnectionTypeState.find(key).isValid()) { + if (!connectionType.isValid()) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f723b17d0..16c04f572 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -689,11 +689,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 + @@ -716,8 +716,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); } @@ -771,7 +771,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, @@ -796,15 +796,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/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 6dbe4f8cd..3512d640f 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -97,6 +97,9 @@ class MenuConfig { } abstract class MenuEntryBase { + bool dismissOnClicked; + + MenuEntryBase({this.dismissOnClicked = false}); List> build(BuildContext context, MenuConfig conf); } @@ -112,9 +115,19 @@ class MenuEntryDivider extends MenuEntryBase { } } -typedef RadioOptionsGetter = List> Function(); +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); +typedef RadioOptionSetter = Future Function( + String oldValue, String newValue); class MenuEntryRadioUtils {} @@ -129,24 +142,28 @@ class MenuEntryRadios extends MenuEntryBase { {required this.text, required this.optionsGetter, required this.curOptionGetter, - required this.optionSetter}) { + required this.optionSetter, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await curOptionGetter(); }(); } - List> get options => optionsGetter(); + List get options => optionsGetter(); RxString get curOption => _curOption; setOption(String option) async { - await optionSetter(option); - final opt = await curOptionGetter(); - if (_curOption.value != opt) { - _curOption.value = opt; + 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, Tuple2 opt) { + BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, @@ -157,7 +174,7 @@ class MenuEntryRadios extends MenuEntryBase { child: Row( children: [ Text( - opt.item1, + opt.text, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, @@ -169,7 +186,7 @@ class MenuEntryRadios extends MenuEntryBase { child: SizedBox( width: 20.0, height: 20.0, - child: Obx(() => opt.item2 == curOption.value + child: Obx(() => opt.value == curOption.value ? Icon( Icons.check, color: conf.commonColor, @@ -180,9 +197,10 @@ class MenuEntryRadios extends MenuEntryBase { ), ), onPressed: () { - if (opt.item2 != curOption.value) { - setOption(opt.item2); + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); } + setOption(opt.value); }, ), ); @@ -206,24 +224,28 @@ class MenuEntrySubRadios extends MenuEntryBase { {required this.text, required this.optionsGetter, required this.curOptionGetter, - required this.optionSetter}) { + required this.optionSetter, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await curOptionGetter(); }(); } - List> get options => optionsGetter(); + List get options => optionsGetter(); RxString get curOption => _curOption; setOption(String option) async { - await optionSetter(option); - final opt = await curOptionGetter(); - if (_curOption.value != opt) { - _curOption.value = opt; + 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, Tuple2 opt) { + BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, @@ -234,7 +256,7 @@ class MenuEntrySubRadios extends MenuEntryBase { child: Row( children: [ Text( - opt.item1, + opt.text, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, @@ -246,7 +268,7 @@ class MenuEntrySubRadios extends MenuEntryBase { child: SizedBox( width: 20.0, height: 20.0, - child: Obx(() => opt.item2 == curOption.value + child: Obx(() => opt.value == curOption.value ? Icon( Icons.check, color: conf.commonColor, @@ -257,9 +279,10 @@ class MenuEntrySubRadios extends MenuEntryBase { ), ), onPressed: () { - if (opt.item2 != curOption.value) { - setOption(opt.item2); + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); } + setOption(opt.value); }, ), ); @@ -303,7 +326,8 @@ typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; - MenuEntrySwitchBase({required this.text}); + MenuEntrySwitchBase({required this.text, required dismissOnClicked}) + : super(dismissOnClicked: dismissOnClicked); RxBool get curOption; Future setOption(bool option); @@ -333,11 +357,20 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { alignment: Alignment.centerRight, child: Obx(() => Switch( value: curOption.value, - onChanged: (v) => setOption(v), + 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); }, ), @@ -352,8 +385,11 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final RxBool _curOption = false.obs; MenuEntrySwitch( - {required String text, required this.getter, required this.setter}) - : super(text: text) { + {required String text, + required this.getter, + required this.setter, + dismissOnClicked = false}) + : super(text: text, dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await getter(); }(); @@ -379,8 +415,11 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final SwitchSetter setter; MenuEntrySwitch2( - {required String text, required this.getter, required this.setter}) - : super(text: text); + {required String text, + required this.getter, + required this.setter, + dismissOnClicked = false}) + : super(text: text, dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @@ -394,10 +433,7 @@ class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; - MenuEntrySubMenu({ - required this.text, - required this.entries, - }); + MenuEntrySubMenu({required this.text, required this.entries}); @override List> build( @@ -438,10 +474,11 @@ class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; - MenuEntryButton({ - required this.childBuilder, - required this.proc, - }); + MenuEntryButton( + {required this.childBuilder, + required this.proc, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked); @override List> build( @@ -461,6 +498,9 @@ class MenuEntryButton extends MenuEntryBase { 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 index 2e8c7fb63..26789ac4f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -290,9 +290,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); bind.sessionRefresh(id: widget.id); }, + dismissOnClicked: true, )); } displayMenu.add(MenuEntryButton( @@ -301,9 +301,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); showSetOSPassword(widget.id, false, widget.ffi.dialogManager); }, + dismissOnClicked: true, )); if (!isWebDesktop) { @@ -314,7 +314,6 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); @@ -323,6 +322,7 @@ class _RemoteMenubarState extends State { } }(); }, + dismissOnClicked: true, )); } @@ -332,9 +332,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); widget.ffi.cursorModel.reset(); }, + dismissOnClicked: true, )); } @@ -346,9 +346,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); bind.sessionCtrlAltDel(id: widget.id); }, + dismissOnClicked: true, )); } @@ -358,9 +358,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); bind.sessionLockScreen(id: widget.id); }, + dismissOnClicked: true, )); if (pi.platform == 'Windows') { @@ -371,13 +371,13 @@ class _RemoteMenubarState extends State { style: style, )), proc: () { - Navigator.pop(context); RxBool blockInput = BlockInputState.find(widget.id); bind.sessionToggleOption( id: widget.id, value: '${blockInput.value ? "un" : ""}block-input'); blockInput.value = !blockInput.value; }, + dismissOnClicked: true, )); } } @@ -392,9 +392,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); }, + dismissOnClicked: true, )); } @@ -406,44 +406,54 @@ class _RemoteMenubarState extends State { MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ - Tuple2(translate('Scale original'), 'original'), - Tuple2(translate('Scale adaptive'), 'adaptive'), + 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 v) async { + optionSetter: (String oldValue, String newValue) async { await bind.sessionPeerOption( - id: widget.id, name: "view-style", value: v); + id: widget.id, name: "view-style", value: newValue); widget.ffi.canvasModel.updateViewStyle(); }), MenuEntryDivider(), MenuEntryRadios( text: translate('Scroll Style'), optionsGetter: () => [ - Tuple2(translate('ScrollAuto'), 'scrollauto'), - Tuple2(translate('Scrollbar'), 'scrollbar'), + 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 v) async { + optionSetter: (String oldValue, String newValue) async { await bind.sessionPeerOption( - id: widget.id, name: "scroll-style", value: v); + id: widget.id, name: "scroll-style", value: newValue); widget.ffi.canvasModel.updateScrollStyle(); }), MenuEntryDivider(), MenuEntryRadios( text: translate('Image Quality'), optionsGetter: () => [ - Tuple2(translate('Good image quality'), 'best'), - Tuple2(translate('Balanced'), 'balanced'), - Tuple2( - translate('Optimize reaction time'), 'low'), + 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 = @@ -451,8 +461,43 @@ class _RemoteMenubarState extends State { if (quality == '') quality = 'balanced'; return quality; }, - optionSetter: (String v) async { - await bind.sessionSetImageQuality(id: widget.id, value: v); + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetImageQuality(id: widget.id, value: newValue); + } + + if (newValue == 'custom') { + final btnCancel = getMsgBoxButton(translate('Cancel'), () { + 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 slider = _ImageCustomQualitySlider( + // id: widget.id, v: RxDouble(initValue)); + final RxDouble sliderValue = RxDouble(initValue); + final slider = Obx(() => Slider( + value: sliderValue.value, + max: 100, + label: sliderValue.value.round().toString(), + onChanged: (double value) { + () async { + await bind.sessionSetCustomImageQuality( + id: widget.id, value: value.toInt()); + final quality = await bind.sessionGetCustomImageQuality( + id: widget.id); + sliderValue.value = + quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : 50.0; + }(); + }, + )); + msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', + slider, [btnCancel]); + } }), MenuEntryDivider(), MenuEntrySwitch( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index da06b7fb9..ba9981db6 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'; @@ -499,8 +500,8 @@ class CanvasModel with ChangeNotifier { _scale = 1.0; if (style == 'adaptive') { - final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); - final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + final s1 = size.width / getDisplayWidth(); + final s2 = size.height / getDisplayHeight(); _scale = s1 < s2 ? s1 : s2; } @@ -529,11 +530,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 { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 167124212..716afacb9 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -143,14 +143,6 @@ 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)) @@ -190,12 +182,34 @@ pub fn session_toggle_option(id: String, value: String) { } } +pub fn session_get_image_quality(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + 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) { + Some(session.get_custom_image_quality()) + } else { + None + } +} + +pub fn session_set_custom_image_quality(id: String, value: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_custom_image_quality(value); + } +} + pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.lock_screen(); From 7cb079afc8dd30caeb538b0334de74e5e873e920 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 23:02:16 +0800 Subject: [PATCH 31/35] flutter_desktop: add debug print Signed-off-by: fufesou --- flutter/lib/models/model.dart | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ba9981db6..962998bbd 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -556,9 +556,19 @@ class CanvasModel with ChangeNotifier { var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { + final xxxx = x - dw * (x / size.width) - _x; + if (xxxx.isInfinite || xxxx.isNaN) { + debugPrint( + 'REMOVE ME ============================ xxxx $x,$dw,$_scale,${size.width},$_x'); + } dxOffset = (x - dw * (x / size.width) - _x).toInt(); } if (dh > size.height) { + final yyyy = y - dh * (y / size.height) - _y; + if (yyyy.isInfinite || yyyy.isNaN) { + debugPrint( + 'REMOVE ME ============================ xxxx $y,$dh,$_scale,${size.height},$_y'); + } dyOffset = (y - dh * (y / size.height) - _y).toInt(); } _x += dxOffset; @@ -926,16 +936,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). From ce1a504e9fdc564a1d42ec8034f01f9baf05f5a3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 23:02:02 -0700 Subject: [PATCH 32/35] flutter_desktop: custom image quality Signed-off-by: fufesou --- flutter/.gitignore | 2 +- flutter/lib/common.dart | 14 +++--- .../lib/desktop/widgets/remote_menubar.dart | 45 ++++++++++--------- flutter/pubspec.yaml | 1 + 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/flutter/.gitignore b/flutter/.gitignore index e5db34d22..ec3fef74e 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -48,7 +48,7 @@ lib/generated_bridge.dart lib/generated_bridge.freezed.dart # 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 e8632caaa..75328c840 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -432,7 +432,7 @@ void msgBox( if (type != "connecting" && type != "success" && !type.contains("nook")) { buttons.insert( 0, - getMsgBoxButton(translate('OK'), () { + msgBoxButton(translate('OK'), () { dialogManager.dismissAll(); // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 if (!type.contains("custom")) { @@ -446,7 +446,7 @@ void msgBox( if (hasCancel) { buttons.insert( 0, - getMsgBoxButton(translate('Cancel'), () { + msgBoxButton(translate('Cancel'), () { dialogManager.dismissAll(); })); } @@ -454,17 +454,17 @@ void msgBox( if (type.contains("hasclose")) { buttons.insert( 0, - getMsgBoxButton(translate('Close'), () { + msgBoxButton(translate('Close'), () { dialogManager.dismissAll(); })); } dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate(title), style: TextStyle(fontSize: 21)), + title: _msgBoxTitle(title), content: Text(translate(text), style: TextStyle(fontSize: 15)), actions: buttons)); } -Widget getMsgBoxButton(String text, void Function() onPressed) { +Widget msgBoxButton(String text, void Function() onPressed) { return ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -479,11 +479,13 @@ Widget getMsgBoxButton(String text, void Function() onPressed) { 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(); dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate(title), style: TextStyle(fontSize: 21)), + title: _msgBoxTitle(title), content: content, actions: buttons)); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 26789ac4f..66edb7a96 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -2,7 +2,7 @@ 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:tuple/tuple.dart'; +import 'package:rxdart/rxdart.dart' as rxdart; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -467,7 +467,7 @@ class _RemoteMenubarState extends State { } if (newValue == 'custom') { - final btnCancel = getMsgBoxButton(translate('Cancel'), () { + final btnCancel = msgBoxButton(translate('Close'), () { widget.ffi.dialogManager.dismissAll(); }); final quality = @@ -475,26 +475,29 @@ class _RemoteMenubarState extends State { final double initValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - // final slider = _ImageCustomQualitySlider( - // id: widget.id, v: RxDouble(initValue)); final RxDouble sliderValue = RxDouble(initValue); - final slider = Obx(() => Slider( - value: sliderValue.value, - max: 100, - label: sliderValue.value.round().toString(), - onChanged: (double value) { - () async { - await bind.sessionSetCustomImageQuality( - id: widget.id, value: value.toInt()); - final quality = await bind.sessionGetCustomImageQuality( - id: widget.id); - sliderValue.value = - quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 50.0; - }(); - }, - )); + 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]); } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a35f1c872..b6ce5d20b 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -73,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 From ec02f9e7213779548a24743794345bc7cb034ccd Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 03:56:12 -0700 Subject: [PATCH 33/35] flutter_desktop: refactor peercard menu Signed-off-by: fufesou --- .../lib/desktop/widgets/peercard_widget.dart | 865 ++++++++++-------- flutter/lib/desktop/widgets/popup_menu.dart | 1 - .../lib/desktop/widgets/remote_menubar.dart | 2 +- 3 files changed, 509 insertions(+), 359 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 4db43398a..3b4a30ee0 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -8,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 } @@ -19,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 @@ -36,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; @@ -66,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))), @@ -185,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, + ), + ); + }), ), ], ), @@ -248,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), ], @@ -262,32 +253,93 @@ 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. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. - void _connect(String id, + void _connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, bool isRDP = false}) async { @@ -308,105 +360,369 @@ 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 == 'connect') { - _connect(id); - } else if (value == 'file') { - _connect(id, isFileTransfer: true); - } else if (value == 'tcp-tunnel') { - _connect(id, isTcpTunneling: true); - } else if (value == 'RDP') { - _connect(id, isRDP: true); - } else if (value == 'remove') { - await bind.mainRemovePeer(id: id); - removePreference(id); - Get.forceAppUpdate(); // TODO use inner model / state - } 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 == '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) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove'), + style: style, + ), + proc: () { + () async { + await bind.mainRemovePeer(id: id); + removePreference(id); + 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)); + 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)); + 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)); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(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)); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(peer.id)); + menuItems.add(_editTagAction(peer.id)); + return menuItems; + } + + @protected + @override + MenuEntryBase _removeAction(String id) { + 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) { @@ -459,205 +775,40 @@ class _PeerCardState extends State<_PeerCard> }); } - void _rename(String id) async { - var isInProgress = false; - var name = await bind.mainGetPeerOption(id: id, key: 'alias'); - var controller = TextEditingController(text: name); - 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'] ?? ""; - } - } - 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()), - ), - ), + 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; - }); - name = controller.text; - 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 { - var items = [ - 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'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } -} - -class FavoritePeerCard extends BasePeerCard { - FavoritePeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.fav); - - Future>> _getPopupMenuItems() async { - var items = [ - 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'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } -} - -class DiscoveredPeerCard extends BasePeerCard { - DiscoveredPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.discovered); - - Future>> _getPopupMenuItems() async { - var items = [ - 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'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } -} - -class AddressBookPeerCard extends BasePeerCard { - AddressBookPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.ab); - - Future>> _getPopupMenuItems() async { - var items = [ - 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'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } } Future> _forceAlwaysRelayMenuItem(String id) async { diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 3512d640f..45e52cf81 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -2,7 +2,6 @@ import 'dart:core'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tuple/tuple.dart'; import './material_mod_popup_menu.dart' as mod_menu; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 66edb7a96..47536011d 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -16,7 +16,7 @@ import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension - static const double height = 24.0; + static const double height = 25.0; static const double dividerHeight = 12.0; } From 9085a938884fa8e1497fa557a3665bf36a8b28a0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 06:18:29 -0700 Subject: [PATCH 34/35] flutter_desktop: fix peer page bugs Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 10 +- flutter/lib/desktop/widgets/peer_widget.dart | 233 ++++++++++-------- .../lib/desktop/widgets/peercard_widget.dart | 30 ++- flutter/lib/models/model.dart | 23 +- flutter/lib/models/peer_model.dart | 58 ++--- flutter/lib/utils/multi_window_manager.dart | 18 +- src/client.rs | 4 +- src/flutter_ffi.rs | 4 +- src/ui_session_interface.rs | 11 +- 9 files changed, 208 insertions(+), 183 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index d366d06ec..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) { 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 3b4a30ee0..114f4146e 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -348,11 +348,11 @@ abstract class BasePeerCard extends StatelessWidget { 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.new_port_forward(id, 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) { @@ -468,7 +468,8 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _removeAction(String id) { + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Remove'), @@ -478,7 +479,8 @@ abstract class BasePeerCard extends StatelessWidget { () async { await bind.mainRemovePeer(id: id); removePreference(id); - Get.forceAppUpdate(); // TODO use inner model / state + await reloadFunc(); + // Get.forceAppUpdate(); // TODO use inner model / state }(); }, dismissOnClicked: true, @@ -614,7 +616,9 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_addFavAction(peer.id)); return menuItems; @@ -638,7 +642,9 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_rmFavAction(peer.id)); return menuItems; @@ -662,9 +668,10 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + })); menuItems.add(_unrememberPasswordAction(peer.id)); - menuItems.add(_addFavAction(peer.id)); return menuItems; } } @@ -686,7 +693,7 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async {})); menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_addFavAction(peer.id)); menuItems.add(_editTagAction(peer.id)); @@ -695,7 +702,8 @@ class AddressBookPeerCard extends BasePeerCard { @protected @override - MenuEntryBase _removeAction(String id) { + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Remove'), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 962998bbd..887bf7d35 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -301,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']; @@ -556,18 +559,18 @@ class CanvasModel with ChangeNotifier { var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { - final xxxx = x - dw * (x / size.width) - _x; - if (xxxx.isInfinite || xxxx.isNaN) { + final X_debugNanOrInfinite = x - dw * (x / size.width) - _x; + if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) { debugPrint( - 'REMOVE ME ============================ xxxx $x,$dw,$_scale,${size.width},$_x'); + 'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x'); } dxOffset = (x - dw * (x / size.width) - _x).toInt(); } if (dh > size.height) { - final yyyy = y - dh * (y / size.height) - _y; - if (yyyy.isInfinite || yyyy.isNaN) { + final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y; + if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) { debugPrint( - 'REMOVE ME ============================ xxxx $y,$dh,$_scale,${size.height},$_y'); + 'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y'); } dyOffset = (y - dh * (y / size.height) - _y).toInt(); } @@ -1249,20 +1252,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; @@ -1270,7 +1273,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/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 b01b84a9d..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'; @@ -37,9 +36,9 @@ class RustDeskMultiWindowManager { 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(); @@ -63,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(); @@ -88,12 +87,9 @@ class RustDeskMultiWindowManager { } } - Future new_port_forward(String remote_id, bool isRDP) async { - final msg = jsonEncode({ - "type": WindowType.PortForward.index, - "id": remote_id, - "isRDP": isRDP - }); + Future newPortForward(String remoteId, bool isRDP) async { + final msg = jsonEncode( + {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/client.rs b/src/client.rs index 25061bcfe..32c0003fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1278,11 +1278,11 @@ impl LoginConfigHandler { /// /// * `username` - The name of the peer. /// * `pi` - The peer info. - pub fn handle_peer_info(&mut self, 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: pi.username.clone(), hostname: pi.hostname.clone(), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 716afacb9..6a3d19880 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -205,8 +205,8 @@ pub fn session_get_custom_image_quality(id: String) -> Option> { } pub fn session_set_custom_image_quality(id: String, value: i32) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.set_custom_image_quality(value); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_custom_image_quality(value); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d89ce2d3b..5ab6089a0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -47,6 +47,11 @@ impl Session { 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 save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } @@ -634,7 +639,7 @@ impl Interface for Session { } } else if !self.is_port_forward() { if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(pi); + self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); self.msgbox("error", "Remote Error", "No Display"); return; @@ -647,9 +652,9 @@ impl Interface for Session { 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); - self.lc.write().unwrap().handle_peer_info(pi); - if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { From 155fa51ff4f933876ede66e6a9e9d819aa96505b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 2 Sep 2022 10:49:20 +0800 Subject: [PATCH 35/35] fix: linux wayland setAlignment crash workaround Signed-off-by: Kingtous --- flutter/pubspec.lock | 12 +++++++----- flutter/pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d2bc7b1a8..61fbfc293 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -858,9 +858,11 @@ packages: screen_retriever: dependency: transitive description: - name: screen_retriever - url: "https://pub.flutter-io.cn" - source: hosted + path: "." + ref: "406b9b0" + resolved-ref: "406b9b038b2c1d779f1e7bf609c8c248be247372" + url: "https://github.com/Kingtous/rustdesk_screen_retriever.git" + source: git version: "0.1.2" scroll_pos: dependency: "direct main" @@ -1244,8 +1246,8 @@ packages: dependency: "direct main" description: path: "." - ref: "247818257b4b37f78bebea1719cee765282b3079" - resolved-ref: "247818257b4b37f78bebea1719cee765282b3079" + ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" + resolved-ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.7" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b6ce5d20b..f2d038af3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 247818257b4b37f78bebea1719cee765282b3079 + ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window