From a149ba832b293a0601296da3c2fc62588db262cb Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 17 Feb 2023 20:07:21 +0100 Subject: [PATCH 01/39] PeerCard. Menu. Move "remove" to last position. --- flutter/lib/common/widgets/peer_card.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f1b94ecdf..7b24ec2e4 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -690,9 +690,6 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadRecentPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } @@ -700,6 +697,9 @@ class RecentPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); return menuItems; } @@ -732,9 +732,6 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadFavPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } @@ -744,6 +741,9 @@ class FavoritePeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); return menuItems; } @@ -775,10 +775,10 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } @@ -811,13 +811,13 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async {})); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } From 02b5085e2b681e4e47075fd67a24d5873f479974 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:07:59 +0100 Subject: [PATCH 02/39] PeerCard. Menu. Make "remove" more visible --- flutter/lib/common/widgets/peer_card.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7b24ec2e4..6ea6a97aa 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -515,9 +515,21 @@ abstract class BasePeerCard extends StatelessWidget { String id, Future Function() reloadFunc, {bool isLan = false}) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove'), + style: style?.copyWith(color: Colors.red), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.delete_forever, color: Colors.red), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -697,6 +709,7 @@ class RecentPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); })); @@ -741,6 +754,7 @@ class FavoritePeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); })); @@ -778,6 +792,7 @@ class DiscoveredPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } @@ -817,6 +832,7 @@ class AddressBookPeerCard extends BasePeerCard { if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } From 6ae0456c45bb2f9419c35e6bb7dd4e2e8e5d0bec Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:11:49 +0100 Subject: [PATCH 03/39] PeerCard. Menu. Change button text "remove" to "delete" --- flutter/lib/common/widgets/peer_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 6ea6a97aa..4a376c588 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -518,7 +518,7 @@ abstract class BasePeerCard extends StatelessWidget { childBuilder: (TextStyle? style) => Row( children: [ Text( - translate('Remove'), + translate('Delete'), style: style?.copyWith(color: Colors.red), ), Expanded( From b139c90dd793e3dd65533aec12ed445f3f12ee51 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 20 Feb 2023 20:25:37 +0100 Subject: [PATCH 04/39] PeerCard. Menu. Make "add to favorites" dynamic --- flutter/lib/common/widgets/peer_card.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 4a376c588..9d9d3d01f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -689,6 +689,9 @@ class RecentPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -705,7 +708,13 @@ class RecentPeerCard extends BasePeerCard { if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } - menuItems.add(_addFavAction(peer.id)); + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } From 819dc4e1a9643df899375240b217dcaccb4a8fd8 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 17 Feb 2023 22:37:08 +0100 Subject: [PATCH 05/39] PeerCard. Menu. "add to favorites" visual indicator --- flutter/lib/common/widgets/peer_card.dart | 36 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 9d9d3d01f..fd0499305 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -565,9 +565,21 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _addFavAction(String id) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Add to Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Add to Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star_outline), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -587,9 +599,21 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _rmFavAction( String id, Future Function() reloadFunc) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove from Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove from Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { From b98581303e7e62f3dcdbfb04737f520345e0fc5d Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:39:01 +0100 Subject: [PATCH 06/39] PeerCard. Menu. Hide "Add to Addressbook" if not logged in --- flutter/lib/common/widgets/peer_card.dart | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index fd0499305..325dfd2ed 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -739,9 +739,15 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async {})); } - if (!gFFI.abModel.idContainBy(peer.id)) { + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); @@ -784,9 +790,16 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async { await bind.mainLoadFavPeers(); })); - if (!gFFI.abModel.idContainBy(peer.id)) { + + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); @@ -821,10 +834,16 @@ class DiscoveredPeerCard extends BasePeerCard { if (Platform.isWindows) { menuItems.add(_createShortCutAction(peer.id)); } - menuItems.add(MenuEntryDivider()); - if (!gFFI.abModel.idContainBy(peer.id)) { + + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; @@ -865,6 +884,7 @@ class AddressBookPeerCard extends BasePeerCard { if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; From 27b8df617d2d0b4fa8ac851ea73851597aefb94c Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 20 Feb 2023 20:13:36 +0100 Subject: [PATCH 07/39] PeerCard. Menu. Remove peer also from favorites when deleted --- flutter/lib/common/widgets/peer_card.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 325dfd2ed..657ba3ccf 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -536,6 +536,10 @@ abstract class BasePeerCard extends StatelessWidget { if (isLan) { // TODO } else { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + } await bind.mainRemovePeer(id: id); } removePreference(id); From 0739820774c92e5d0688ec7397c559a720becf69 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Tue, 21 Feb 2023 15:58:00 +0100 Subject: [PATCH 08/39] PeerCard. Menu. Add menu item "add to favorite" to DiscoveredPeerCard --- flutter/lib/common/widgets/peer_card.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 657ba3ccf..db2a90d9e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -827,6 +827,9 @@ class DiscoveredPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -839,6 +842,12 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + if (gFFI.userModel.userName.isNotEmpty) { // if (!gFFI.abModel.idContainBy(peer.id)) { // menuItems.add(_addToAb(peer)); From 8c3be1c8ced9eba83d682c02ac15bdfbdeaeb840 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Tue, 21 Feb 2023 18:01:43 +0100 Subject: [PATCH 09/39] PeerCard. Menu. Add label to text input on "rename" --- flutter/lib/common/widgets/peer_card.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index db2a90d9e..8d4d58772 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -682,8 +682,9 @@ abstract class BasePeerCard extends StatelessWidget { child: TextFormField( controller: controller, autofocus: true, - decoration: - const InputDecoration(border: OutlineInputBorder()), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: translate('Name')), ), ), ), From 135e0c8a99be4e9c629110bb65ee6e62cc8b45a3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 21:57:51 +0800 Subject: [PATCH 10/39] add mutex guard for arboard funcs Signed-off-by: fufesou --- src/common.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 02d367b5e..5f24fd5c3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -52,6 +52,11 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); +} + pub fn global_init() -> bool { #[cfg(target_os = "linux")] { @@ -96,7 +101,11 @@ pub fn check_clipboard( ) -> Option { let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; - if let Ok(content) = ctx.get_text() { + let content = { + let _lock = ARBOARD_MTX.lock().unwrap(); + ctx.get_text() + }; + if let Ok(content) = content { if content.len() < 2_000_000 && !content.is_empty() { let changed = content != *old.lock().unwrap(); if changed { @@ -174,6 +183,7 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; *old.lock().unwrap() = content.clone(); + let _lock = ARBOARD_MTX.lock().unwrap(); allow_err!(ctx.set_text(content)); log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } From 080c98769437e0a931fc9073bf309e000e19a075 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Thu, 23 Feb 2023 22:17:59 +0100 Subject: [PATCH 11/39] Update fr.rs --- src/lang/fr.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..50cb29938 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), ("Change ID", "Changer d'ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Votre nouvel ID"), + ("length %min% to %max%", "longueur de %min% à %max%"), + ("starts with a letter", "commence par une lettre"), + ("allowed characters", "caractères autorisés"), ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Website", "Site Web"), ("About", "À propos de"), @@ -89,7 +89,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Hidden Files", "Afficher les fichiers cachés"), ("Receive", "Recevoir"), ("Send", "Envoyer"), - ("Refresh File", "Actualiser le fichier"), + ("Refresh File", "Rafraîchir le contenu"), ("Local", "Local"), ("Remote", "Distant"), ("Remote Computer", "Ordinateur distant"), From 91a2a5b56e283b208a3822c6fccd81c3fb8ea599 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 9 Feb 2023 15:53:51 +0800 Subject: [PATCH 12/39] win resolution && api Signed-off-by: 21pages --- flutter/lib/models/model.dart | 41 +++++++++++++ libs/hbb_common/protos/message.proto | 10 ++++ src/flutter.rs | 25 ++++++++ src/flutter_ffi.rs | 8 ++- src/platform/mod.rs | 10 +++- src/platform/windows.rs | 90 +++++++++++++++++++++++++++- src/server/connection.rs | 48 +++++++++++++++ src/server/video_service.rs | 15 ++++- src/ui_session_interface.rs | 12 ++++ 9 files changed, 255 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5ef72a0af..f4efe2f08 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -270,6 +270,7 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); + handleResolutions(peerId, evt["resolutions"]); notifyListeners(); } @@ -437,10 +438,35 @@ class FfiModel with ChangeNotifier { } Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; + handleResolutions(peerId, evt["resolutions"]); } notifyListeners(); } + handleResolutions(String id, dynamic resolutions) { + try { + final List dynamicArray = jsonDecode(resolutions as String); + List arr = List.empty(growable: true); + for (int i = 0; i < dynamicArray.length; i++) { + var width = dynamicArray[i]["width"]; + var height = dynamicArray[i]["height"]; + if (width is int && width > 0 && height is int && height > 0) { + arr.add(Resolution(width, height)); + } + } + arr.sort((a, b) { + if (b.width != a.width) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); + _pi.resolutions = arr; + } catch (e) { + debugPrint("Failed to parse resolutions:$e"); + } + } + /// Handle the peer info synchronization event based on [evt]. handleSyncPeerInfo(Map evt, String peerId) async { if (evt['displays'] != null) { @@ -458,6 +484,9 @@ class FfiModel with ChangeNotifier { } _pi.displays = newDisplays; stateGlobal.displaysCount.value = _pi.displays.length; + if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { + _display = _pi.displays[_pi.currentDisplay]; + } } notifyListeners(); } @@ -1532,6 +1561,17 @@ class Display { } } +class Resolution { + int width = 0; + int height = 0; + Resolution(this.width, this.height); + + @override + String toString() { + return 'Resolution($width,$height)'; + } +} + class Features { bool privacyMode = false; } @@ -1545,6 +1585,7 @@ class PeerInfo { int currentDisplay = 0; List displays = []; Features features = Features(); + List resolutions = []; } const canvasKey = 'canvas'; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 2a3fd05b4..be3a1e51e 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -90,6 +90,7 @@ message PeerInfo { int32 conn_id = 8; Features features = 9; SupportedEncoding encoding = 10; + SupportedResolutions resolutions = 11; } message LoginResponse { @@ -416,6 +417,13 @@ message Cliprdr { } } +message Resolution { + int32 width = 1; + int32 height = 2; +} + +message SupportedResolutions { repeated Resolution resolutions = 1; } + message SwitchDisplay { int32 display = 1; sint32 x = 2; @@ -423,6 +431,7 @@ message SwitchDisplay { int32 width = 4; int32 height = 5; bool cursor_embedded = 6; + SupportedResolutions resolutions = 7; } message PermissionInfo { @@ -597,6 +606,7 @@ message Misc { bool portable_service_running = 20; SwitchSidesRequest switch_sides_request = 21; SwitchBack switch_back = 22; + Resolution change_resolution = 24; } } diff --git a/src/flutter.rs b/src/flutter.rs index d366a0eda..ea73eb925 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -480,6 +480,7 @@ impl InvokeUiSession for FlutterHandler { features.insert("privacy_mode", 0); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + let resolutions = serialize_resolutions(&pi.resolutions.resolutions); *self.peer_info.write().unwrap() = pi.clone(); self.push_event( "peer_info", @@ -492,6 +493,7 @@ impl InvokeUiSession for FlutterHandler { ("version", &pi.version), ("features", &features), ("current_display", &pi.current_display.to_string()), + ("resolutions", &resolutions), ], ); } @@ -529,6 +531,7 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { + let resolutions = serialize_resolutions(&display.resolutions.resolutions); self.push_event( "switch_display", vec![ @@ -548,6 +551,7 @@ impl InvokeUiSession for FlutterHandler { } .to_string(), ), + ("resolutions", &resolutions), ], ); } @@ -861,6 +865,27 @@ pub fn set_cur_session_id(id: String) { } } +#[inline] +fn serialize_resolutions(resolutions: &Vec) -> String { + #[derive(Debug, serde::Serialize)] + struct ResolutionSerde { + width: i32, + height: i32, + } + + let mut v = vec![]; + resolutions + .iter() + .map(|r| { + v.push(ResolutionSerde { + width: r.width, + height: r.height, + }) + }) + .count(); + serde_json::ser::to_string(&v).unwrap_or("".to_string()) +} + #[no_mangle] #[cfg(not(feature = "flutter_texture_render"))] pub fn session_get_rgba_size(id: *const char) -> usize { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 14906d568..8a8bf4de4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,7 +529,13 @@ pub fn session_switch_sides(id: String) { } } -pub fn session_set_size(_id: String, _width: i32, _height: i32) { +pub fn session_change_resolution(id: String, width: i32, height: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.change_resolution(width, height); + } +} + +pub fn session_set_size(_id: String, _width: i32, _height: i32) { #[cfg(feature = "flutter_texture_render")] if let Some(session) = SESSIONS.write().unwrap().get_mut(&_id) { session.set_size(_width, _height); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ed5fcfaa1..ad058d4c0 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -74,5 +74,13 @@ mod tests { assert!(!get_cursor_pos().is_none()); } } -} + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[test] + fn test_resolution() { + let name = r"\\.\DISPLAY1"; + println!("current:{:?}", current_resolution(name)); + println!("change:{:?}", change_resolution(name, 2880, 1800)); + println!("resolutions:{:?}", resolutions(name)); + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bd6a1fc4c..6b3f8013c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -5,7 +5,9 @@ use crate::license::*; use hbb_common::{ allow_err, bail, config::{self, Config}, - log, sleep, timeout, tokio, + log, + message_proto::Resolution, + sleep, timeout, tokio, }; use std::io::prelude::*; use std::{ @@ -1784,3 +1786,89 @@ pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { .spawn()?; Ok(()) } + +pub fn resolutions(name: &str) -> Vec { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + let mut v = vec![]; + let mut num = 0; + loop { + if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 { + break; + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + if !v.contains(&r) { + v.push(r); + } + num += 1; + } + v + } +} + +pub fn current_resolution(name: &str) -> ResultType { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + dm.dmSize = std::mem::size_of::() as _; + let wname = wide_string(name); + if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { + bail!( + "failed to get currrent resolution, errno={}", + GetLastError() + ); + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + Ok(r) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) { + bail!("EnumDisplaySettingsW failed, errno={}", GetLastError()); + } + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + dm.dmPelsWidth = width as _; + dm.dmPelsHeight = height as _; + dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; + let res = ChangeDisplaySettingsExW( + wname.as_ptr(), + &mut dm, + NULL as _, + CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, + NULL, + ); + if res != DISP_CHANGE_SUCCESSFUL { + bail!( + "ChangeDisplaySettingsExW failed, res={}, errno={}", + res, + GetLastError() + ); + } + Ok(()) + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index d2eb21ee5..85fcb676b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -123,6 +123,7 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, } @@ -228,6 +229,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + origin_resolution: Default::default(), audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, @@ -533,6 +535,8 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + conn.reset_resolution(); ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); if let Some(s) = conn.server.upgrade() { s.write().unwrap().remove_connection(&conn.inner); @@ -881,6 +885,16 @@ impl Connection { ..Default::default() }) .into(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: video_service::get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } let mut sub_service = false; if self.file_transfer.is_some() { @@ -1597,6 +1611,26 @@ impl Connection { return false; } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::ChangeResolution(r)) => { + if self.keyboard { + if let Ok(name) = video_service::get_current_display_name() { + if let Ok(current) = crate::platform::current_resolution(&name) { + if let Err(e) = crate::platform::change_resolution( + &name, + r.width as _, + r.height as _, + ) { + log::error!("change resolution failed:{:?}", e); + } else { + if !self.origin_resolution.contains_key(&name) { + self.origin_resolution.insert(name, current); + } + } + } + } + } + } _ => {} }, Some(message::Union::AudioFrame(frame)) => { @@ -1937,6 +1971,20 @@ impl Connection { } } } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn reset_resolution(&self) { + self.origin_resolution + .iter() + .map(|(name, r)| { + if let Err(e) = + crate::platform::change_resolution(&name, r.width as _, r.height as _) + { + log::error!("change resolution failed:{:?}", e); + } + }) + .count(); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 52b1717c4..a9a9fd9ab 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -356,7 +356,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType<()> { width: c.width as _, height: c.height as _, cursor_embedded: capture_cursor_embedded(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + resolutions: Some(SupportedResolutions { + resolutions: get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..SupportedResolutions::default() + }) + .into(), ..Default::default() }); let mut msg_out = Message::new(); @@ -992,6 +1001,10 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { get_current_display_2(try_get_displays()?) } +pub fn get_current_display_name() -> ResultType { + Ok(get_current_display_2(try_get_displays()?)?.2.name()) +} + #[cfg(windows)] fn start_uac_elevation_check() { static START: Once = Once::new(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5fbf2f4e7..fd5a7d9c0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -713,6 +713,18 @@ impl Session { } } + pub fn change_resolution(&self, width: i32, height: i32) { + let mut misc = Misc::new(); + misc.set_change_resolution(Resolution { + width, + height, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(Data::Message(msg)); + } + pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } From 18a66749a1a7d0b767cb05f197bebc309c7bcbd0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 11 Feb 2023 19:04:33 +0800 Subject: [PATCH 13/39] linux x11 resolution Signed-off-by: 21pages --- Cargo.lock | 72 +++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + libs/scrap/src/common/x11.rs | 4 +- libs/scrap/src/x11/display.rs | 7 ++++ libs/scrap/src/x11/ffi.rs | 18 +++++++++ libs/scrap/src/x11/iter.rs | 30 +++++++++++++++ src/platform/linux.rs | 55 +++++++++++++++++++++++++- 7 files changed, 180 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8483cbac1..a2cdf91a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,14 +1159,38 @@ dependencies = [ "zvariant", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.47", + "quote 1.0.21", + "strsim 0.9.3", + "syn 1.0.105", ] [[package]] @@ -1183,13 +1207,24 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", "quote 1.0.21", "syn 1.0.105", ] @@ -1389,6 +1424,18 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "derive_setters" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b" +dependencies = [ + "darling 0.10.2", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -3585,7 +3632,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro-crate 1.2.1", "proc-macro2 1.0.47", "quote 1.0.21", @@ -4944,6 +4991,7 @@ dependencies = [ "winreg 0.10.1", "winres", "wol-rs", + "xrandr-parser", ] [[package]] @@ -5469,6 +5517,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "strsim" version = "0.10.0" @@ -6965,6 +7019,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "xrandr-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1" +dependencies = [ + "derive_setters", + "serde 1.0.149", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index c20366983..f93f776a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" +xrandr-parser = "0.3.0" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 61112bff7..6e3fc94fb 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -1,4 +1,4 @@ -use crate::{x11, common::TraitCapturer}; +use crate::{common::TraitCapturer, x11}; use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); @@ -90,6 +90,6 @@ impl Display { } pub fn name(&self) -> String { - "".to_owned() + self.0.name() } } diff --git a/libs/scrap/src/x11/display.rs b/libs/scrap/src/x11/display.rs index 0c5ba5035..a33903caa 100644 --- a/libs/scrap/src/x11/display.rs +++ b/libs/scrap/src/x11/display.rs @@ -9,6 +9,7 @@ pub struct Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, } #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] @@ -25,12 +26,14 @@ impl Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, ) -> Display { Display { server, default, rect, root, + name, } } @@ -52,4 +55,8 @@ impl Display { pub fn root(&self) -> xcb_window_t { self.root } + + pub fn name(&self) -> String { + self.name.clone() + } } diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs index 500f57615..b34fed416 100644 --- a/libs/scrap/src/x11/ffi.rs +++ b/libs/scrap/src/x11/ffi.rs @@ -65,6 +65,21 @@ extern "C" { ) -> xcb_randr_monitor_info_iterator_t; pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); + + pub fn xcb_get_atom_name( + c: *mut xcb_connection_t, + atom: xcb_atom_t, + ) -> xcb_get_atom_name_cookie_t; + + pub fn xcb_get_atom_name_reply( + c: *mut xcb_connection_t, + cookie: xcb_get_atom_name_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *const xcb_get_atom_name_reply_t; + + pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8; + + pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32; } pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; @@ -78,6 +93,9 @@ pub type xcb_timestamp_t = u32; pub type xcb_colormap_t = u32; pub type xcb_shm_seg_t = u32; pub type xcb_drawable_t = u32; +pub type xcb_get_atom_name_cookie_t = u32; +pub type xcb_get_atom_name_reply_t = u32; +pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t; #[repr(C)] pub struct xcb_setup_t { diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs index 406c27352..28609376b 100644 --- a/libs/scrap/src/x11/iter.rs +++ b/libs/scrap/src/x11/iter.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::ptr; use std::rc::Rc; @@ -64,6 +65,7 @@ impl Iterator for DisplayIter { if inner.rem != 0 { unsafe { let data = &*inner.data; + let name = get_atom_name(self.server.raw(), data.name); let display = Display::new( self.server.clone(), @@ -75,6 +77,7 @@ impl Iterator for DisplayIter { h: data.height, }, root, + name, ); xcb_randr_monitor_info_next(inner); @@ -91,3 +94,30 @@ impl Iterator for DisplayIter { } } } + +fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String { + let empty = "".to_owned(); + if atom == 0 { + return empty; + } + unsafe { + let mut e: xcb_generic_error_t = std::mem::zeroed(); + let reply = xcb_get_atom_name_reply( + conn, + xcb_get_atom_name(conn, atom), + &mut ((&mut e) as *mut xcb_generic_error_t) as _, + ); + if reply == std::ptr::null() { + return empty; + } + let length = xcb_get_atom_name_name_length(reply); + let name = xcb_get_atom_name_name(reply); + let mut v = vec![0u8; length as _]; + std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _); + libc::free(reply as *mut _); + if let Ok(s) = CString::new(v) { + return s.to_string_lossy().to_string(); + } + empty + } +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 32c32efb9..08e343d49 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,7 +1,7 @@ use super::{CursorData, ResultType}; use hbb_common::libc::{c_char, c_int, c_long, c_void}; pub use hbb_common::platform::linux::*; -use hbb_common::{allow_err, bail, log}; +use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use std::{ cell::RefCell, path::PathBuf, @@ -10,6 +10,7 @@ use std::{ Arc, }, }; +use xrandr_parser::Parser; type Xdo = *const c_void; @@ -641,3 +642,55 @@ pub fn get_double_click_time() -> u32 { double_click_time } } + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + let mut parser = Parser::new(); + if parser.parse().is_ok() { + if let Ok(connector) = parser.get_connector(name) { + if let Ok(resolutions) = &connector.available_resolutions() { + for r in resolutions { + if let Ok(width) = r.horizontal.parse::() { + if let Ok(height) = r.vertical.parse::() { + let resolution = Resolution { + width, + height, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let mut parser = Parser::new(); + parser.parse().map_err(|e| anyhow!(e))?; + let connector = parser.get_connector(name).map_err(|e| anyhow!(e))?; + let r = connector.current_resolution(); + let width = r.horizontal.parse::()?; + let height = r.vertical.parse::()?; + Ok(Resolution { + width, + height, + ..Default::default() + }) +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + std::process::Command::new("xrandr") + .args(vec![ + "--output", + name, + "--mode", + &format!("{}x{}", width, height), + ]) + .spawn()?; + Ok(()) +} From 5b8e51d6b981e1b0cad80edcb56ab1cc5d6a8fb3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 12 Feb 2023 16:09:41 +0800 Subject: [PATCH 14/39] mac resolution Signed-off-by: 21pages --- src/platform/macos.mm | 111 ++++++++++++++++++++++++++++++++++++++++++ src/platform/macos.rs | 73 ++++++++++++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 789404cb6..443351469 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -40,3 +40,114 @@ extern "C" float BackingScaleFactor() { if (s) return [s backingScaleFactor]; return 1; } + +// https://github.com/jhford/screenresolution/blob/master/cg_utils.c +// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m + +extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + for (int i = 0; i < *numModes && i < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + widths[i] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[i] = (uint32_t)CGDisplayModeGetHeight(mode); + } + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t *height) { + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); + if (mode == NULL) { + return false; + } + *width = (uint32_t)CGDisplayModeGetWidth(mode); + *height = (uint32_t)CGDisplayModeGetHeight(mode); + CGDisplayModeRelease(mode); + return true; +} + +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode); + // my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels + // are made up and possibly non-sensical + if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 96; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 64; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 48; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 32; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 30; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 16; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) { + depth = 8; + } + CFRelease(pixelEncoding); + return depth; +} + +bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { + CGError rc; + CGDisplayConfigRef config; + rc = CGBeginDisplayConfiguration(&config); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGConfigureDisplayWithDisplayMode(config, display, mode, NULL); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGCompleteDisplayConfiguration(config, kCGConfigureForSession); + if (rc != kCGErrorSuccess) { + return false; + } + return true; +} + + +extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) +{ + bool ret = false; + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return ret; + } + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return ret; + } + int numModes = CFArrayGetCount(allModes); + CGDisplayModeRef bestMode = NULL; + for (int i = 0; i < numModes; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (width == CGDisplayModeGetWidth(mode) && + height == CGDisplayModeGetHeight(mode) && + bitDepth(currentMode) == bitDepth(mode) && + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) { + ret = setDisplayToMode(display, mode); + break; + } + } + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return ret; +} \ No newline at end of file diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3e19cca28..025274840 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,7 +17,7 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::{allow_err, bail, log}; +use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -34,6 +34,16 @@ extern "C" { static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; + fn MacGetModes( + display: u32, + widths: *mut u32, + heights: *mut u32, + max: u32, + numModes: *mut u32, + ) -> BOOL; + fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; + fn MacSetMode(display: u32, width: u32, height: u32) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -594,3 +604,64 @@ pub fn handle_application_should_open_untitled_file() { } } } + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + if let Ok(display) = name.parse::() { + let mut num = 0; + unsafe { + if YES == MacGetModeNum(display, &mut num) { + let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); + let mut realNum = 0; + if YES + == MacGetModes( + display, + widths.as_mut_ptr(), + heights.as_mut_ptr(), + num, + &mut realNum, + ) + { + if realNum <= num { + for i in 0..realNum { + let resolution = Resolution { + width: widths[i as usize] as _, + height: heights[i as usize] as _, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + let (mut width, mut height) = (0, 0); + if NO == MacGetMode(display, &mut width, &mut height) { + bail!("MacGetMode failed"); + } + Ok(Resolution { + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + if NO == MacSetMode(display, width as _, height as _) { + bail!("MacSetMode failed"); + } + } + Ok(()) +} From 4338451f6f7f64a9f84c38d690c11b1b78b44e7e Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 14:30:29 +0800 Subject: [PATCH 15/39] refactor remote menubar with MenuBar for submenu Signed-off-by: 21pages --- flutter/lib/common.dart | 16 + .../desktop/pages/desktop_setting_page.dart | 34 +- .../lib/desktop/widgets/remote_menubar.dart | 2791 +++++++++-------- src/lang/ca.rs | 2 + 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/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/gr.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/kz.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 36 files changed, 1582 insertions(+), 1325 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6d3e4c3b7..ddd9ea1ac 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1814,3 +1814,19 @@ class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { @override bool get allowImplicitScrolling => false; } + +Widget futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + debugPrint(snapshot.error.toString()); + } + return Container(); + } + }); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 971c713ce..ffe707cf0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -319,7 +319,7 @@ class _GeneralState extends State<_General> { bind.mainSetOption(key: 'audio-input', value: device); } - return _futureBuilder(future: () async { + return futureBuilder(future: () async { List devices = (await bind.mainGetSoundInputs()).toList(); if (Platform.isWindows) { devices.insert(0, 'System Sound'); @@ -346,7 +346,7 @@ class _GeneralState extends State<_General> { } Widget record(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String customDirectory = await bind.mainGetOption(key: 'video-save-directory'); String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); @@ -399,7 +399,7 @@ class _GeneralState extends State<_General> { } Widget language() { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String langs = await bind.mainGetLangs(); String lang = bind.mainGetLocalOption(key: kCommConfKeyLang); return {'langs': langs, 'lang': lang}; @@ -487,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget _permissions(context, bool stopService) { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'access-mode'); }(), hasData: (data) { String accessMode = data! as String; @@ -744,7 +744,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return [ _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), - _futureBuilder( + futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); String port = await bind.mainGetOption(key: 'direct-access-port'); @@ -805,7 +805,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'whitelist'); }(), hasData: (data) { RxBool hasWhitelist = (data as String).isNotEmpty.obs; @@ -931,7 +931,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } server(bool enabled) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOptions(); }(), hasData: (data) { // Setting page is not modal, oldOptions should only be used when getting options, never when setting. @@ -1366,7 +1366,7 @@ class _About extends StatefulWidget { class _AboutState extends State<_About> { @override Widget build(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final buildDate = await bind.mainGetBuildDate(); @@ -1500,7 +1500,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, bool enabled = true, Icon? checkedIcon, bool? fakeValue}) { - return _futureBuilder( + return futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { bool value = option2bool(key, data.toString()); @@ -1633,22 +1633,6 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child, ).marginOnly(left: _kContentHSubMargin); } -Widget _futureBuilder( - {required Future? future, required Widget Function(dynamic data) hasData}) { - return FutureBuilder( - future: future, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return hasData(snapshot.data!); - } else { - if (snapshot.hasError) { - debugPrint(snapshot.error.toString()); - } - return Container(); - } - }); -} - Widget _lock( bool locked, String label, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 45857aa45..993d02683 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,11 +1,9 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; @@ -23,7 +21,6 @@ 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; import './kb_layout_type_chooser.dart'; class MenubarState { @@ -102,6 +99,11 @@ class _MenubarTheme { // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; + + static const double buttonSize = 32; + static const double buttonHMargin = 3; + static const double buttonVMargin = 6; + static const double iconRadius = 8; } typedef DismissFunc = void Function(); @@ -280,7 +282,7 @@ class RemoteMenubar extends StatefulWidget { final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function() onEnterOrLeaveImageCleaner; - const RemoteMenubar({ + RemoteMenubar({ Key? key, required this.id, required this.ffi, @@ -296,7 +298,6 @@ class RemoteMenubar extends StatefulWidget { class _RemoteMenubarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - window_size.Screen? _screen; final _fractionX = 0.5.obs; final _dragging = false.obs; @@ -347,7 +348,6 @@ class _RemoteMenubarState extends State { @override Widget build(BuildContext context) { // No need to use future builder here. - _updateScreen(); return Align( alignment: Alignment.topCenter, child: Obx(() => show.value @@ -375,6 +375,577 @@ class _RemoteMenubarState extends State { }); } + Widget _buildMenubar(BuildContext context) { + final List menubarItems = []; + if (!isWebDesktop) { + menubarItems.add(_PinMenu(state: widget.state)); + menubarItems.add( + _FullscreenMenu(state: widget.state, setFullscreen: _setFullscreen)); + menubarItems.add(_MobileActionMenu(ffi: widget.ffi)); + } + menubarItems.add(_MonitorMenu(id: widget.id, ffi: widget.ffi)); + menubarItems + .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state)); + menubarItems.add(_DisplayMenu( + id: widget.id, + ffi: widget.ffi, + state: widget.state, + setFullscreen: _setFullscreen, + )); + menubarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + if (!isWeb) { + menubarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); + menubarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); + } + menubarItems.add(_RecordMenu()); + menubarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + )), + ), + _buildDraggableShowHide(context), + ], + ); + } +} + +class _PinMenu extends StatelessWidget { + final MenubarState state; + const _PinMenu({Key? key, required this.state}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => _IconMenuButton( + assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg", + tooltip: state.pin ? 'Unpin menubar' : 'Pin menubar', + onPressed: state.switchPin, + color: state.pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: + state.pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, + ), + ); + } +} + +class _FullscreenMenu extends StatelessWidget { + final MenubarState state; + final Function(bool) setFullscreen; + bool get isFullscreen => stateGlobal.fullscreen; + const _FullscreenMenu( + {Key? key, required this.state, required this.setFullscreen}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + tooltip: isFullscreen ? 'Exit Fullscreen' : 'Fullscreen', + onPressed: () => setFullscreen(!isFullscreen), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MobileActionMenu extends StatelessWidget { + final FFI ffi; + const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (!ffi.ffiModel.isPeerAndroid) return Offstage(); + return _IconMenuButton( + assetName: 'assets/actions_mobile.svg', + tooltip: 'Mobile Actions', + onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MonitorMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _MonitorMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (stateGlobal.displaysCount.value < 2) return Offstage(); + return _IconSubmenuButton( + icon: icon(), + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuStyle: MenuStyle( + padding: + MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), + menuChildren: [Row(children: displays(context))]); + } + + icon() { + final pi = ffi.ffiModel.pi; + return Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.9), + child: Obx(() { + RxInt display = CurrentDisplayState.find(id); + return Text( + '${display.value + 1}/${pi.displays.length}', + style: const TextStyle(color: Colors.white, fontSize: 8), + ); + }), + ) + ], + ); + } + + List displays(BuildContext context) { + final List rowChildren = []; + final pi = ffi.ffiModel.pi; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add(_IconMenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + tooltip: "", + hMargin: 6, + vMargin: 12, + icon: Container( + alignment: AlignmentDirectional.center, + constraints: const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.5 /*2.5*/), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ) + ], + ), + ), + onPressed: () { + _menuDismissCallback(ffi); + RxInt display = CurrentDisplayState.find(id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: id, value: i); + } + }, + )); + } + return rowChildren; + } +} + +class _ControlMenu extends StatelessWidget { + final String id; + final FFI ffi; + final MenubarState state; + _ControlMenu( + {Key? key, required this.id, required this.ffi, required this.state}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + svg: "assets/actions.svg", + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ffi: ffi, + menuChildren: [ + osPassword(), + transferFile(context), + tcpTunneling(context), + note(), + Divider(), + ctrlAltDel(), + restart(), + blockUserInput(), + switchSides(), + refresh(), + ]); + } + + osPassword() { + return _MenuItemButton( + child: Text(translate('OS Password')), + trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), + ffi: ffi, + onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager)); + } + + _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) { + submit() { + 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(); + } + + 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: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + transferFile(BuildContext context) { + return _MenuItemButton( + child: Text(translate('Transfer File')), + ffi: ffi, + onPressed: () => connect(context, id, isFileTransfer: true)); + } + + tcpTunneling(BuildContext context) { + return _MenuItemButton( + child: Text(translate('TCP Tunneling')), + ffi: ffi, + onPressed: () => connect(context, id, isTcpTunneling: true)); + } + + note() { + final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn"); + final visible = auditServer.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Note')), + ffi: ffi, + onPressed: () => _showAuditDialog(id, ffi.dialogManager), + ); + } + + _showAuditDialog(String id, dialogManager) async { + final controller = TextEditingController(); + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + if (text != '') { + bind.sessionSendNote(id: id, note: text); + } + close(); + } + + late final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + close(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration.collapsed( + hintText: 'input note here', + ), + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + )), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + ctrlAltDel() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + (pi.platform == kPeerPlatformLinux || pi.sasEnabled); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text('${translate("Insert")} Ctrl + Alt + Del'), + ffi: ffi, + onPressed: () => bind.sessionCtrlAltDel(id: id)); + } + + restart() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Restart Remote Device')), + ffi: ffi, + onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); + } + + blockUserInput() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = + perms['keyboard'] != false && pi.platform == kPeerPlatformWindows; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Obx(() => Text(translate( + '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), + ffi: ffi, + onPressed: () { + RxBool blockInput = BlockInputState.find(id); + bind.sessionToggleOption( + id: id, value: '${blockInput.value ? 'un' : ''}block-input'); + blockInput.value = !blockInput.value; + }); + } + + switchSides() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && + version_cmp(pi.version, '1.2.0') >= 0; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Switch Sides')), + ffi: ffi, + onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager)); + } + + void _showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + refresh() { + final pi = ffi.ffiModel.pi; + final visible = pi.version.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Refresh')), + ffi: ffi, + onPressed: () => bind.sessionRefresh(id: id)); + } +} + +class _DisplayMenu extends StatefulWidget { + final String id; + final FFI ffi; + final MenubarState state; + final Function(bool) setFullscreen; + _DisplayMenu( + {Key? key, + required this.id, + required this.ffi, + required this.state, + required this.setFullscreen}) + : super(key: key); + + @override + State<_DisplayMenu> createState() => _DisplayMenuState(); +} + +class _DisplayMenuState extends State<_DisplayMenu> { + window_size.Screen? _screen; + + bool get isFullscreen => stateGlobal.fullscreen; + + int get windowId => stateGlobal.windowId; + + Map get perms => widget.ffi.ffiModel.permissions; + + PeerInfo get pi => widget.ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + _updateScreen(); + return _IconSubmenuButton( + svg: "assets/display.svg", + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [ + adjustWindow(), + viewStyle(), + scrollStyle(), + imageQuality(), + codec(), + resolutions(), + Divider(), + showRemoteCursor(), + zoomCursor(), + showQualityMonitor(), + mute(), + fileCopyAndPaste(), + disableClipboard(), + lockAfterSessionEnd(), + privacyMode(), + ]); + } + + adjustWindow() { + final visible = _isWindowCanBeAdjusted(); + if (!visible) return Offstage(); + return Column( + children: [ + _MenuItemButton( + child: Text(translate('Adjust Window')), + onPressed: _doAdjustWindow, + ffi: widget.ffi), + Divider(), + ], + ); + } + + _doAdjustWindow() async { + await _updateScreen(); + if (_screen != null) { + widget.setFullscreen(false); + double scale = _screen!.scaleFactor; + final wndRect = await WindowController.fromWindowId(windowId).getFrame(); + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. + // https://stackoverflow.com/a/7561083 + double magicWidth = + wndRect.right - wndRect.left - mediaSize.width * scale; + double magicHeight = + wndRect.bottom - wndRect.top - mediaSize.height * scale; + + final canvasModel = widget.ffi.canvasModel; + final width = (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = (canvasModel.getDisplayHeight() * canvasModel.scale + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; + double left = wndRect.left + (wndRect.width - width) / 2; + double top = wndRect.top + (wndRect.height - height) / 2; + + Rect frameRect = _screen!.frame; + if (!isFullscreen) { + frameRect = _screen!.visibleFrame; + } + if (left < frameRect.left) { + left = frameRect.left; + } + if (top < frameRect.top) { + top = frameRect.top; + } + if ((left + width) > frameRect.right) { + left = frameRect.right - width; + } + if ((top + height) > frameRect.bottom) { + top = frameRect.bottom - height; + } + await WindowController.fromWindowId(windowId) + .setFrame(Rect.fromLTWH(left, top, width, height)); + } + } + _updateScreen() async { final v = await rustDeskWinManager.call( WindowType.Main, kWindowGetWindowInfo, ''); @@ -395,638 +966,11 @@ class _RemoteMenubarState extends State { } } - Widget _buildPointerTrackWidget(Widget child) { - return Listener( - onPointerHover: (PointerHoverEvent e) => - widget.ffi.inputModel.lastMousePos = e.position, - child: MouseRegion( - child: child, - ), - ); - } - - _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos(); - - Widget _buildMenubar(BuildContext context) { - final List menubarItems = []; - if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); - if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(MenuButton( - tooltip: translate('Mobile Actions'), - child: SvgPicture.asset( - "assets/actions_mobile.svg", - color: Colors.white, - ), - onPressed: () { - widget.ffi.dialogManager - .toggleMobileActionsOverlay(ffi: widget.ffi); - }, - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - )); - } + _isWindowCanBeAdjusted() { + if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + return false; } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); - if (!isWeb) { - menubarItems.add(_buildChat(context)); - menubarItems.add(_buildVoiceCall(context)); - } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); - return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.blueColor)), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(10), - ), - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3), - ...menubarItems, - SizedBox(width: 3) - ], - ), - ), - ), - _buildDraggableShowHide(context), - ], - ), - ); - } - - Widget _buildPinMenubar(BuildContext context) { - return Obx( - () => MenuButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - child: SvgPicture.asset( - pin ? "assets/pinned.svg" : "assets/unpinned.svg", - color: Colors.white, - ), - color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!, - hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, - ), - ); - } - - Widget _buildFullscreen(BuildContext context) { - return MenuButton( - tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), - onPressed: () { - _setFullscreen(!isFullscreen); - }, - child: SvgPicture.asset( - isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", - color: Colors.white, - ), - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - ); - } - - Widget _buildMonitor(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final monitor = mod_menu.PopupMenuButton( - tooltip: translate('Select Monitor'), - position: mod_menu.PopupMenuPosition.under, - icon: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - Padding( - padding: const EdgeInsets.only(bottom: 3.9), - child: Obx(() { - RxInt display = CurrentDisplayState.find(widget.id); - return Text( - '${display.value + 1}/${pi.displays.length}', - style: const TextStyle(color: Colors.white, fontSize: 8), - ); - }), - ) - ], - ), - itemBuilder: (BuildContext context) { - final List rowChildren = []; - for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add(MenuButton( - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ) - ], - ), - ), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - )); - } - return >[ - mod_menu.PopupMenuItem( - height: _MenubarTheme.height, - padding: EdgeInsets.zero, - child: _buildPointerTrackWidget( - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren, - ), - ), - ) - ]; - }, - ); - - return Obx(() => Offstage( - offstage: stateGlobal.displaysCount.value < 2, - child: monitor, - )); - } - - Widget _buildControl(BuildContext context) { - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/actions.svg", - color: Colors.white, - ), - tooltip: translate('Control Actions'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getControlMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildDisplay(BuildContext context) { - return FutureBuilder(future: () async { - widget.state.viewStyle.value = - await bind.sessionGetViewStyle(id: widget.id) ?? ''; - final supportedHwcodec = - await bind.sessionSupportedHwcodec(id: widget.id); - return {'supportedHwcodec': supportedHwcodec}; - }(), builder: (context, snapshot) { - if (snapshot.hasData) { - return Obx(() { - final remoteCount = RemoteCountState.find().value; - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - tooltip: translate('Display Settings'), - position: mod_menu.PopupMenuPosition.under, - menuWrapper: _buildPointerTrackWidget, - itemBuilder: (BuildContext context) => - _getDisplayMenu(snapshot.data!, remoteCount) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - }); - } else { - return const Offstage(); - } - }); - } - - Widget _buildKeyboard(BuildContext context) { - // Do not support peer 1.1.9. - if (stateGlobal.grabKeyboard) { - bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); - return Offstage(); - } - - FfiModel ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) { - return Offstage(); - } - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/keyboard.svg", - color: Colors.white, - ), - tooltip: translate('Keyboard Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getKeyboardMenu() - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildRecording(BuildContext context) { - return Consumer(builder: ((context, value, child) { - if (value.permissions['recording'] != false) { - return Consumer( - builder: (context, value, child) => MenuButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - child: SvgPicture.asset( - "assets/rec.svg", - color: Colors.white, - ), - color: - value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, - hoverColor: value.start - ? _MenubarTheme.hoverRedColor - : _MenubarTheme.hoverBlueColor, - ), - ); - } else { - return Offstage(); - } - })); - } - - Widget _buildClose(BuildContext context) { - return MenuButton( - tooltip: translate('Close'), - onPressed: () { - clientClose(widget.id, widget.ffi.dialogManager); - }, - child: SvgPicture.asset( - "assets/close.svg", - color: Colors.white, - ), - color: _MenubarTheme.redColor, - hoverColor: _MenubarTheme.hoverRedColor, - ); - } - - final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context) { - FfiModel ffiModel = Provider.of(context); - return mod_menu.PopupMenuButton( - key: _chatButtonKey, - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/chat.svg", - color: Colors.white, - ), - tooltip: translate('Chat'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getChatMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _getVoiceCallIcon() { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return SvgPicture.asset( - "assets/call_wait.svg", - color: Colors.white, - ); - - case VoiceCallStatus.connected: - return SvgPicture.asset( - "assets/call_end.svg", - color: Colors.white, - ); - default: - return const Offstage(); - } - } - - String? _getVoiceCallTooltip() { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return "Waiting"; - case VoiceCallStatus.connected: - return "Disconnect"; - default: - return null; - } - } - - Widget _buildVoiceCall(BuildContext context) { - return Obx( - () { - final tooltipText = _getVoiceCallTooltip(); - return tooltipText == null - ? const Offstage() - : MenuButton( - child: _getVoiceCallIcon(), - tooltip: translate(tooltipText), - onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), - color: _MenubarTheme.redColor, - hoverColor: _MenubarTheme.hoverRedColor, - ); - }, - ); - } - - List> _getChatMenu(BuildContext context) { - final List> chatMenu = []; - const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); - chatMenu.addAll([ - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Text chat'), - style: style, - ), - proc: () { - RenderBox? renderBox = - _chatButtonKey.currentContext?.findRenderObject() as RenderBox?; - - Offset? initPos; - if (renderBox != null) { - final pos = renderBox.localToGlobal(Offset.zero); - initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); - } - - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Voice call'), - style: style, - ), - proc: () { - // Request a voice call. - bind.sessionRequestVoiceCall(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - ), - ]); - return chatMenu; - } - - List> _getControlMenu(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final perms = widget.ffi.ffiModel.permissions; - final peer_version = widget.ffi.ffiModel.pi.version; - const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); - final List> displayMenu = []; - displayMenu.addAll([ - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Text( - translate('OS Password'), - style: style, - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - showSetOSPassword( - widget.id, false, widget.ffi.dialogManager); - })), - )) - ], - )), - proc: () { - showSetOSPassword(widget.id, false, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Transfer File'), - style: style, - ), - proc: () { - connect(context, widget.id, isFileTransfer: true); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('TCP Tunneling'), - style: style, - ), - padding: padding, - proc: () { - connect(context, widget.id, isTcpTunneling: true); - }, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ]); - // {handler.get_audit_server() &&
  • {translate('Note')}
  • } - final auditServer = - bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); - if (auditServer.isNotEmpty) { - displayMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Note'), - style: style, - ), - proc: () { - showAuditDialog(widget.id, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ); - } - displayMenu.add(MenuEntryDivider()); - if (perms['keyboard'] != false) { - if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding, - dismissCallback: _menuDismissCallback)); - } - } - if (perms['restart'] != false && - (pi.platform == kPeerPlatformLinux || - pi.platform == kPeerPlatformWindows || - pi.platform == kPeerPlatformMacOS)) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Restart Remote Device'), - style: style, - ), - proc: () { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - - if (perms['keyboard'] != false) { - displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding, - dismissCallback: _menuDismissCallback)); - - if (pi.platform == kPeerPlatformWindows) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Obx(() => Text( - translate( - '${BlockInputState.find(widget.id).value ? 'Unb' : 'B'}lock user input'), - style: style, - )), - proc: () { - RxBool blockInput = BlockInputState.find(widget.id); - bind.sessionToggleOption( - id: widget.id, - value: '${blockInput.value ? 'un' : ''}block-input'); - blockInput.value = !blockInput.value; - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - if (pi.platform != kPeerPlatformAndroid && - pi.platform != kPeerPlatformMacOS && // unsupport yet - version_cmp(peer_version, '1.2.0') >= 0) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Switch Sides'), - style: style, - ), - proc: () => - showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - } - - if (pi.version.isNotEmpty) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Refresh'), - style: style, - ), - proc: () { - bind.sessionRefresh(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - - if (!isWebDesktop) { - // if (perms['keyboard'] != false && perms['clipboard'] != false) { - // displayMenu.add(MenuEntryButton( - // childBuilder: (TextStyle? style) => Text( - // translate('Paste'), - // style: style, - // ), - // proc: () { - // () async { - // ClipboardData? data = - // await Clipboard.getData(Clipboard.kTextPlain); - // if (data != null && data.text != null) { - // bind.sessionInputString(id: widget.id, value: data.text ?? ''); - // } - // }(); - // }, - // padding: padding, - // dismissOnClicked: true, - // dismissCallback: _menuDismissCallback, - // )); - // } - } - return displayMenu; - } - - bool _isWindowCanBeAdjusted(int remoteCount) { + final remoteCount = RemoteCountState.find().value; if (remoteCount != 1) { return false; } @@ -1052,312 +996,277 @@ class _RemoteMenubarState extends State { selfHeight > (requiredHeight * scale); } - List> _getDisplayMenu( - dynamic futureData, int remoteCount) { - const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); - final peer_version = widget.ffi.ffiModel.pi.version; - final displayMenu = [ - RemoteMenuEntry.viewStyle( - widget.id, - widget.ffi, - padding, - dismissCallback: _menuDismissCallback, - rxViewStyle: widget.state.viewStyle, - ), - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Image Quality'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Good image quality'), + viewStyle() { + return futureBuilder(future: () async { + final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + widget.state.viewStyle.value = viewStyle; + return viewStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetViewStyle(id: widget.id, value: value); + widget.state.viewStyle.value = value; + widget.ffi.canvasModel.updateViewStyle(); + } + + return Column(children: [ + _RadioMenuButton( + child: Text(translate('Scale original')), + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scale adaptive')), + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + scrollStyle() { + final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + return futureBuilder(future: () async { + final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? ''; + return scrollStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChange(String? value) async { + if (value == null) return; + await bind.sessionSetScrollStyle(id: widget.id, value: value); + widget.ffi.canvasModel.updateScrollStyle(); + } + + final enabled = widget.ffi.canvasModel.imageOverflow.value; + return Column(children: [ + _RadioMenuButton( + child: Text(translate('ScrollAuto')), + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scrollbar')), + value: kRemoteScrollStyleBar, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + imageQuality() { + return futureBuilder(future: () async { + final imageQuality = + await bind.sessionGetImageQuality(id: widget.id) ?? ''; + return imageQuality; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetImageQuality(id: widget.id, value: value); + } + + return SubmenuButton( + child: Text(translate('Image Quality')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Good image quality')), value: kRemoteImageQualityBest, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Balanced'), + _RadioMenuButton( + child: Text(translate('Balanced')), value: kRemoteImageQualityBalanced, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Optimize reaction time'), + _RadioMenuButton( + child: Text(translate('Optimize reaction time')), value: kRemoteImageQualityLow, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Custom'), + _RadioMenuButton( + child: Text(translate('Custom')), value: kRemoteImageQualityCustom, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetImageQuality(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetImageQuality(id: widget.id, value: newValue); - } - - double qualityInitValue = 50; - double fpsInitValue = 30; - bool qualitySet = false; - bool fpsSet = false; - setCustomValues({double? quality, double? fps}) async { - if (quality != null) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: quality.toInt()); - } - if (fps != null) { - fpsSet = true; - await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); - } - if (!qualitySet) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: qualityInitValue.toInt()); - } - if (!fpsSet) { - fpsSet = true; - await bind.sessionSetCustomFps( - id: widget.id, fps: fpsInitValue.toInt()); - } - } - - if (newValue == kRemoteImageQualityCustom) { - final btnClose = dialogButton('Close', onPressed: () async { - await setCustomValues(); - widget.ffi.dialogManager.dismissAll(); - }); - - // quality - final quality = - await bind.sessionGetCustomImageQuality(id: widget.id); - qualityInitValue = quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 50.0; - const qualityMinValue = 10.0; - const qualityMaxValue = 100.0; - if (qualityInitValue < qualityMinValue) { - qualityInitValue = qualityMinValue; - } - if (qualityInitValue > qualityMaxValue) { - qualityInitValue = qualityMaxValue; - } - final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final debouncerQuality = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(quality: v); - }, - initialValue: qualityInitValue, - ); - final qualitySlider = Obx(() => Row( - children: [ - Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: qualityMaxValue, - divisions: 18, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - ), - SizedBox( - width: 40, - child: Text( - '${qualitySliderValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )) - ], - )); - // fps - final fpsOption = - await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); - fpsInitValue = - fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; - if (fpsInitValue < 10 || fpsInitValue > 120) { - fpsInitValue = 30; - } - final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final debouncerFps = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(fps: v); - }, - initialValue: qualityInitValue, - ); - bool? direct; - try { - direct = ConnectionTypeState.find(widget.id).direct.value == - ConnectionType.strDirect; - } catch (_) {} - final fpsSlider = Offstage( - offstage: - (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(peer_version, '1.2.0') < 0, - child: Row( - children: [ - Obx((() => Slider( - value: fpsSliderValue.value, - min: 10, - max: 120, - divisions: 22, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - ))), - SizedBox( - width: 40, - child: Obx(() => Text( - '${fpsSliderValue.value.round()}', - style: const TextStyle(fontSize: 15), - ))), - SizedBox( - width: 50, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - ), - ); - - final content = Column( - children: [qualitySlider, fpsSlider], - ); - msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnClose]); - } - }, - padding: padding, - ), - MenuEntryDivider(), - ]; - - if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { - displayMenu.insert( - 2, - MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), - value: kRemoteScrollStyleAuto, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - enabled: widget.ffi.canvasModel.imageOverflow, - ), - MenuEntryRadioOption( - text: translate('Scrollbar'), - value: kRemoteScrollStyleBar, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - enabled: widget.ffi.canvasModel.imageOverflow, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetScrollStyle(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetScrollStyle(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); + groupValue: groupValue, + onChanged: (value) { + onChanged(value); + _customImageQualityDialog(); }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - displayMenu.insert(3, MenuEntryDivider()); - - if (_isWindowCanBeAdjusted(remoteCount)) { - displayMenu.insert( - 0, - MenuEntryDivider(), - ); - displayMenu.insert( - 0, - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - child: Text( - translate('Adjust Window'), - style: style, - )), - proc: () { - () async { - await _updateScreen(); - if (_screen != null) { - _setFullscreen(false); - double scale = _screen!.scaleFactor; - final wndRect = - await WindowController.fromWindowId(windowId).getFrame(); - final mediaSize = MediaQueryData.fromWindow(ui.window).size; - // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. - // https://stackoverflow.com/a/7561083 - double magicWidth = - wndRect.right - wndRect.left - mediaSize.width * scale; - double magicHeight = - wndRect.bottom - wndRect.top - mediaSize.height * scale; - - final canvasModel = widget.ffi.canvasModel; - final width = - (canvasModel.getDisplayWidth() * canvasModel.scale + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = - (canvasModel.getDisplayHeight() * canvasModel.scale + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; - double left = wndRect.left + (wndRect.width - width) / 2; - double top = wndRect.top + (wndRect.height - height) / 2; - - Rect frameRect = _screen!.frame; - if (!isFullscreen) { - frameRect = _screen!.visibleFrame; - } - if (left < frameRect.left) { - left = frameRect.left; - } - if (top < frameRect.top) { - top = frameRect.top; - } - if ((left + width) > frameRect.right) { - left = frameRect.right - width; - } - if ((top + height) > frameRect.bottom) { - top = frameRect.bottom - height; - } - await WindowController.fromWindowId(windowId) - .setFrame(Rect.fromLTWH(left, top, width, height)); - } - }(); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + ffi: widget.ffi, ), - ); + ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), + ); + }); + } + + _customImageQualityDialog() async { + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + id: widget.id, fps: fpsInitValue.toInt()); } } - /// Show Codec Preference - if (bind.mainHasHwcodec()) { + final btnClose = dialogButton('Close', onPressed: () async { + await setCustomValues(); + widget.ffi.dialogManager.dismissAll(); + }); + + // quality + final quality = await bind.sessionGetCustomImageQuality(id: widget.id); + qualityInitValue = + quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; + const qualityMinValue = 10.0; + const qualityMaxValue = 100.0; + if (qualityInitValue < qualityMinValue) { + qualityInitValue = qualityMinValue; + } + if (qualityInitValue > qualityMaxValue) { + qualityInitValue = qualityMaxValue; + } + final RxDouble qualitySliderValue = RxDouble(qualityInitValue); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(quality: v); + }, + initialValue: qualityInitValue, + ); + final qualitySlider = Obx(() => Row( + children: [ + Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 18, + onChanged: (double value) { + qualitySliderValue.value = value; + debouncerQuality.value = value; + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualitySliderValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )); + // fps + final fpsOption = + await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); + fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 10 || fpsInitValue > 120) { + fpsInitValue = 30; + } + final RxDouble fpsSliderValue = RxDouble(fpsInitValue); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(fps: v); + }, + initialValue: qualityInitValue, + ); + bool? direct; + try { + direct = ConnectionTypeState.find(widget.id).direct.value == + ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(pi.version, '1.2.0') < 0, + child: Row( + children: [ + Obx((() => Slider( + value: fpsSliderValue.value, + min: 10, + max: 120, + divisions: 22, + onChanged: (double value) { + fpsSliderValue.value = value; + debouncerFps.value = value; + }, + ))), + SizedBox( + width: 40, + child: Obx(() => Text( + '${fpsSliderValue.value.round()}', + style: const TextStyle(fontSize: 15), + ))), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], + ); + msgBoxCommon( + widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); + } + + codec() { + return futureBuilder(future: () async { + final supportedHwcodec = + await bind.sessionSupportedHwcodec(id: widget.id); + final codecPreference = + await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ?? + ''; + return { + 'supportedHwcodec': supportedHwcodec, + 'codecPreference': codecPreference + }; + }(), hasData: (data) { final List codecs = []; try { - final Map codecsJson = jsonDecode(futureData['supportedHwcodec']); + final Map codecsJson = jsonDecode(data['supportedHwcodec']); final h264 = codecsJson['h264'] ?? false; final h265 = codecsJson['h265'] ?? false; codecs.add(h264); @@ -1365,385 +1274,655 @@ class _RemoteMenubarState extends State { } catch (e) { debugPrint("Show Codec Preference err=$e"); } - if (codecs.length == 2 && (codecs[0] || codecs[1])) { - displayMenu.add(MenuEntryRadios( - text: translate('Codec Preference'), - optionsGetter: () { - final list = [ - MenuEntryRadioOption( - text: translate('Auto'), - value: 'auto', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryRadioOption( - text: 'VP9', - value: 'vp9', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ]; - if (codecs[0]) { - list.add(MenuEntryRadioOption( - text: 'H264', - value: 'h264', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - if (codecs[1]) { - list.add(MenuEntryRadioOption( - text: 'H265', - value: 'h265', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - return list; - }, - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: 'codec-preference', value: newValue); - bind.sessionChangePreferCodec(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + final visible = bind.mainHasHwcodec() && + codecs.length == 2 && + (codecs[0] || codecs[1]); + if (!visible) return Offstage(); + final groupValue = data['codecPreference'] as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionPeerOption( + id: widget.id, name: 'codec-preference', value: value); + bind.sessionChangePreferCodec(id: widget.id); } - } - displayMenu.add(MenuEntryDivider()); - /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbedded) { - displayMenu.add(RemoteMenuEntry.showRemoteCursor( - widget.id, - padding, - dismissCallback: _menuDismissCallback, - )); - } - - /// Show remote cursor scaling with image - if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: opt); - state.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: opt); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ); - }()); - } - - /// Show quality monitor - displayMenu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - - final perms = widget.ffi.ffiModel.permissions; - final pi = widget.ffi.ffiModel.pi; - - if (perms['audio'] != false) { - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - } - - if (Platform.isWindows && - pi.platform == kPeerPlatformWindows && - perms['file'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Allow file copy and paste', 'enable-file-transfer', padding, true)); - } - - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) { - displayMenu.add(RemoteMenuEntry.disableClipboard( - widget.id, - padding, - dismissCallback: _menuDismissCallback, - )); - } - displayMenu.add(_createSwitchMenuEntry( - 'Lock after session end', 'lock-after-session-end', padding, true)); - if (pi.features.privacyMode) { - displayMenu.add(MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Privacy mode'), - getter: () { - return PrivacyModeState.find(widget.id); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'privacy-mode'); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - } - return displayMenu; - } - - List> _getKeyboardMenu() { - final List> keyboardMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () { - List list = []; - List modes = [ - KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), - KeyboardModeMenu(key: 'map', menu: 'Map mode'), - KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), - ]; - - for (KeyboardModeMenu mode in modes) { - if (bind.sessionIsKeyboardModeSupported( - id: widget.id, mode: mode.key)) { - if (mode.key == 'translate') { - if (Platform.isLinux || - widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) { - continue; - } - } - var text = translate(mode.menu); - if (mode.key == 'translate') { - text = '$text beta'; - } - list.add(MenuEntryRadioOption(text: text, value: mode.key)); - } - } - return list; - }, - curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); - }, - ) - ]; - final localPlatform = - getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform); - if (localPlatform != '') { - keyboardMenu.add(MenuEntryDivider()); - keyboardMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Obx(() => RichText( - text: TextSpan( - text: '${translate('Local keyboard type')}: ', - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: KBLayoutType.value, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.settings), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - showKBLayoutTypeChooser( - localPlatform, widget.ffi.dialogManager); - }, - ), - ), - )) - ], - )), - proc: () {}, - padding: EdgeInsets.zero, - dismissOnClicked: false, - dismissCallback: _menuDismissCallback, - ), - ); - } - return keyboardMenu; - } - - MenuEntrySwitch _createSwitchMenuEntry( - String text, String option, EdgeInsets? padding, bool dismissOnClicked) { - return RemoteMenuEntry.createSwitchMenuEntry( - widget.id, text, option, padding, dismissOnClicked, - dismissCallback: _menuDismissCallback); - } -} - -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) { - submit() { - 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(); - } - - 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: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showAuditDialog(String id, dialogManager) async { - final controller = TextEditingController(); - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - if (text != '') { - bind.sessionSendNote(id: id, note: text); - } - close(); - } - - late final focusNode = FocusNode( - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt.logicalKey.keyLabel == 'Enter') { - if (evt is RawKeyDownEvent) { - int pos = controller.selection.base.offset; - controller.text = - '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; - controller.selection = - TextSelection.fromPosition(TextPosition(offset: pos + 1)); - } - return KeyEventResult.handled; - } - if (evt.logicalKey.keyLabel == 'Esc') { - if (evt is RawKeyDownEvent) { - close(); - } - return KeyEventResult.handled; - } else { - return KeyEventResult.ignored; - } - }, - ); - - return CustomAlertDialog( - title: Text(translate('Note')), - content: SizedBox( - width: 250, - height: 120, - child: TextField( - autofocus: true, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - decoration: const InputDecoration.collapsed( - hintText: 'input note here', + return SubmenuButton( + child: Text(translate('Codec')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Auto')), + value: 'auto', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - // inputFormatters: [ - // LengthLimitingTextInputFormatter(16), - // // FilteringTextInputFormatter(RegExp(r'[a-zA-z][a-zA-z0-9\_]*'), allow: true) - // ], - maxLines: null, - maxLength: 256, - controller: controller, - focusNode: focusNode, - )), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit) - ], - onSubmit: submit, - onCancel: close, - ); - }); -} + _RadioMenuButton( + child: Text(translate('VP9')), + value: 'vp9', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H264')), + value: 'h264', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H265')), + value: 'h265', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); + }); + } -void showConfirmSwitchSidesDialog( - String id, OverlayDialogManager dialogManager) async { - dialogManager.show((setState, close) { - submit() async { - await bind.sessionSwitchSides(id: id); - closeConnection(id: id); + resolutions() { + final resolutions = widget.ffi.ffiModel.pi.resolutions; + final visible = widget.ffi.ffiModel.permissions["keyboard"] != false && + resolutions.length > 1; + if (!visible) return Offstage(); + final display = widget.ffi.ffiModel.display; + final groupValue = "${display.width}x${display.height}"; + onChanged(String? value) async { + if (value == null) return; + final list = value.split('x'); + if (list.length == 2) { + final w = int.tryParse(list[0]); + final h = int.tryParse(list[1]); + if (w != null && h != null) { + await bind.sessionChangeResolution( + id: widget.id, width: w, height: h); + } + } } - return CustomAlertDialog( - content: msgboxContent('info', 'Switch Sides', - 'Please confirm if you want to share your desktop?'), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), + return SubmenuButton( + menuChildren: resolutions + .map((e) => _RadioMenuButton( + value: '${e.width}x${e.height}', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + child: Text('${e.width}x${e.height}'))) + .toList() + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList(), + child: Text(translate("Resolution"))); + } + + showRemoteCursor() { + final visible = !widget.ffi.canvasModel.cursorEmbedded; + if (!visible) return Offstage(); + final state = ShowRemoteCursorState.find(widget.id); + final option = 'show-remote-cursor'; + return _CheckboxMenuButton( + value: state.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Show remote cursor'))); + } + + zoomCursor() { + final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + final option = 'zoom-cursor'; + final peerState = PeerBoolOption.find(widget.id, option); + return _CheckboxMenuButton( + value: peerState.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + peerState.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Zoom cursor'))); + } + + showQualityMonitor() { + final option = 'show-quality-monitor'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }, + ffi: widget.ffi, + child: Text(translate('Show quality monitor'))); + } + + mute() { + final visible = perms['audio'] != false; + if (!visible) return Offstage(); + final option = 'disable-audio'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Mute'))); + } + + fileCopyAndPaste() { + final visible = Platform.isWindows && + pi.platform == kPeerPlatformWindows && + perms['file'] != false; + if (!visible) return Offstage(); + final option = 'enable-file-transfer'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Allow file copy and paste'))); + } + + disableClipboard() { + final visible = perms['keyboard'] != false && perms['clipboard'] != false; + if (!visible) return Offstage(); + final option = 'disable-clipboard'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Disable clipboard'))); + } + + lockAfterSessionEnd() { + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + final option = 'lock-after-session-end'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Lock after session end'))); + } + + privacyMode() { + bool visible = perms['keyboard'] != false && pi.features.privacyMode; + if (!visible) return Offstage(); + final option = 'privacy-mode'; + final rxValue = PrivacyModeState.find(widget.id); + return _CheckboxMenuButton( + value: rxValue.value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Privacy mode'))); + } +} + +class _KeyboardMenu extends StatelessWidget { + final String id; + final FFI ffi; + _KeyboardMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + PeerInfo get pi => ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + var ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) return Offstage(); + // Do not support peer 1.1.9. + if (stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: id, value: 'map'); + return Offstage(); + } + return _IconSubmenuButton( + svg: "assets/keyboard.svg", + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [mode(), localKeyboardType()]); + } + + mode() { + return futureBuilder(future: () async { + return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy'; + }(), hasData: (data) { + final groupValue = data as String; + List modes = [ + KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), + KeyboardModeMenu(key: 'map', menu: 'Map mode'), + KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + ]; + List<_RadioMenuButton> list = []; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetKeyboardMode(id: id, value: value); + } + + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { + if (mode.key == 'translate') { + if (Platform.isLinux || pi.platform == kPeerPlatformLinux) { + continue; + } + } + var text = translate(mode.menu); + if (mode.key == 'translate') { + text = '$text beta'; + } + list.add(_RadioMenuButton( + child: Text(text), + value: mode.key, + groupValue: groupValue, + onChanged: onChanged, + ffi: ffi, + )); + } + } + return Column(children: list); + }); + } + + localKeyboardType() { + final localPlatform = getLocalPlatformForKBLayoutType(pi.platform); + final visible = localPlatform != ''; + if (!visible) return Offstage(); + return Column( + children: [ + Divider(), + _MenuItemButton( + child: Text( + '${translate('Local keyboard type')}: ${KBLayoutType.value}'), + trailingIcon: const Icon(Icons.settings), + ffi: ffi, + onPressed: () => + showKBLayoutTypeChooser(localPlatform, ffi.dialogManager), + ) ], - onSubmit: submit, - onCancel: close, ); - }); + } +} + +class _ChatMenu extends StatefulWidget { + final String id; + final FFI ffi; + _ChatMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State<_ChatMenu> createState() => _ChatMenuState(); +} + +class _ChatMenuState extends State<_ChatMenu> { + // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`. + final chatButtonKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [textChat(), voiceCall()]); + } + + textChat() { + return _MenuItemButton( + child: Text(translate('Text chat')), + ffi: widget.ffi, + onPressed: () { + RenderBox? renderBox = + chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); + } + + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); + }); + } + + voiceCall() { + return _MenuItemButton( + child: Text(translate('Voice call')), + ffi: widget.ffi, + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); + } +} + +class _VoiceCallMenu extends StatelessWidget { + final String id; + final FFI ffi; + _VoiceCallMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () { + final String tooltip; + final String icon; + switch (ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + tooltip = "Waiting"; + icon = "assets/call_wait.svg"; + break; + case VoiceCallStatus.connected: + tooltip = "Disconnect"; + icon = "assets/call_end.svg"; + break; + default: + return Offstage(); + } + return _IconMenuButton( + assetName: icon, + tooltip: tooltip, + onPressed: () => bind.sessionCloseVoiceCall(id: id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor); + }, + ); + } +} + +class _RecordMenu extends StatelessWidget { + const _RecordMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var ffi = Provider.of(context); + final visible = ffi.permissions['recording'] != false; + if (!visible) return Offstage(); + return Consumer( + builder: (context, value, child) => _IconMenuButton( + assetName: 'assets/rec.svg', + tooltip: + value.start ? 'Stop session recording' : 'Start session recording', + onPressed: () => value.toggle(), + color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, + ), + ); + } +} + +class _CloseMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _CloseMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: 'assets/close.svg', + tooltip: 'Close', + onPressed: () => clientClose(id, ffi.dialogManager), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, + ); + } +} + +class _IconMenuButton extends StatefulWidget { + final String? assetName; + final Widget? icon; + final String tooltip; + final Color color; + final Color hoverColor; + final VoidCallback? onPressed; + final double? hMargin; + final double? vMargin; + const _IconMenuButton({ + Key? key, + this.assetName, + this.icon, + required this.tooltip, + required this.color, + required this.hoverColor, + required this.onPressed, + this.hMargin, + this.vMargin, + }) : super(key: key); + + @override + State<_IconMenuButton> createState() => _IconMenuButtonState(); +} + +class _IconMenuButtonState extends State<_IconMenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.assetName != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.assetName!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + return SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: MenuItemButton( + style: ButtonStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + onPressed: widget.onPressed, + child: Tooltip( + message: translate(widget.tooltip), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon))), + ), + ).marginSymmetric( + horizontal: widget.hMargin ?? _MenubarTheme.buttonHMargin, + vertical: widget.vMargin ?? _MenubarTheme.buttonVMargin); + } +} + +class _IconSubmenuButton extends StatefulWidget { + final String? svg; + final Widget? icon; + final Color color; + final Color hoverColor; + final List menuChildren; + final MenuStyle? menuStyle; + final FFI ffi; + + _IconSubmenuButton( + {Key? key, + this.svg, + this.icon, + required this.color, + required this.hoverColor, + required this.menuChildren, + required this.ffi, + this.menuStyle}) + : super(key: key); + + @override + State<_IconSubmenuButton> createState() => _IconSubmenuButtonState(); +} + +class _IconSubmenuButtonState extends State<_IconSubmenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.svg != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.svg!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + return SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: SubmenuButton( + menuStyle: widget.menuStyle, + style: ButtonStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon)), + menuChildren: widget.menuChildren + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList())) + .marginSymmetric( + horizontal: _MenubarTheme.buttonHMargin, + vertical: _MenubarTheme.buttonVMargin); + } +} + +class _MenuItemButton extends StatelessWidget { + final VoidCallback? onPressed; + final Widget? trailingIcon; + final Widget? child; + final FFI ffi; + _MenuItemButton( + {Key? key, + this.onPressed, + this.trailingIcon, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MenuItemButton( + key: key, + onPressed: onPressed != null + ? () { + _menuDismissCallback(ffi); + onPressed?.call(); + } + : null, + trailingIcon: trailingIcon, + child: child); + } +} + +class _CheckboxMenuButton extends StatelessWidget { + final bool? value; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _CheckboxMenuButton( + {Key? key, + required this.value, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CheckboxMenuButton( + key: key, + value: value, + child: child, + onChanged: onChanged != null + ? (bool? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } +} + +class _RadioMenuButton extends StatelessWidget { + final T value; + final T? groupValue; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _RadioMenuButton( + {Key? key, + required this.value, + required this.groupValue, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return RadioMenuButton( + value: value, + groupValue: groupValue, + child: child, + onChanged: onChanged != null + ? (T? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } } class _DraggableShowHide extends StatefulWidget { @@ -1843,3 +2022,15 @@ class KeyboardModeMenu { KeyboardModeMenu({required this.key, required this.menu}); } + +_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); + +Widget _buildPointerTrackWidget(Widget child, FFI ffi) { + return Listener( + onPointerHover: (PointerHoverEvent e) => + ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: child, + ), + ); +} diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 45c552848..71aa39337 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4824ac5e9..818e63203 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止语音通话"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), + ("Codec", "编解码"), + ("Resolution", "分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2761e45e..be0ffa7f4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2020a2b6f..150a57715 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 7cf563fc3..c9c25df2b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Sprachanruf beenden"), ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), ("Reconnect", "Erneut verbinden"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c22532440..bb2615efc 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3ce2860f0..d7e43b6bf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Detener llamada de voz"), ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), ("Reconnect", "Reconectar"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 70051f3e8..d8fcff436 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..cb4d8d69f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index b7ebf4577..c18e6c07b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 21ab28214..557e3faf0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f48de17f6..1a34e6fea 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 4c63106da..7256b13d8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), ("Reconnect", "Riconnetti"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b291a6e7a..d6354c1c9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d63e83187..dc57c8bf9 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b8b9eb1df..6698b2c5f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 1a806c803..545e1ec2e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Stop spraakoproep"), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2b29c7cb2..eea46accb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e91cd3909..ee1561123 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b0fe9175d..7b16bdf34 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index d0232ba37..315eadd2a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe5d708ad..6d212490b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("Reconnect", "Переподключить"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 458002f4c..462a78ab6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2abd1870f..0eb1949fe 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6b739e8ab..2fc5dfe0d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90a435fd7..17882094c 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a98ea6346..250cf3405 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 61c2b5d28..dcdcc1289 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 236ee5e8d..a1eb34c54 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f2a34e212..09c40a83f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 84e74716f..ca1193eaa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止語音聊天"), ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), ("Reconnect", "重連"), + ("Codec", "編解碼"), + ("Resolution", "分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 0c4caf4db..b48385e6e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 19e1184d9..61d7c0b8a 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } From f5edf44f0f9c28ea865647e9596b1b5bccbdae2b Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 21:31:00 +0800 Subject: [PATCH 16/39] remote menubar theme Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 993d02683..b32520fa6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -408,18 +408,32 @@ class _RemoteMenubarState extends State { ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, - child: MenuBar( - children: [ - SizedBox(width: _MenubarTheme.buttonHMargin), - ...menubarItems, - SizedBox(width: _MenubarTheme.buttonHMargin) - ], + child: Theme( + data: themeData(), + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + ), )), ), _buildDraggableShowHide(context), ], ); } + + ThemeData themeData() { + return Theme.of(context).copyWith( + menuButtonTheme: MenuButtonThemeData( + style: ButtonStyle( + minimumSize: MaterialStatePropertyAll(Size(64, 36)), + textStyle: MaterialStatePropertyAll( + TextStyle(fontWeight: FontWeight.normal)))), + dividerTheme: DividerThemeData(space: 4), + ); + } } class _PinMenu extends StatelessWidget { From 69f16ccd9f086cebfa2053f9df48c6d96b42c7d3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 21:57:45 +0800 Subject: [PATCH 17/39] delay 3s to adjust window after changing resolution Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b32520fa6..4f9a227bd 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1351,6 +1351,14 @@ class _DisplayMenuState extends State<_DisplayMenu> { if (w != null && h != null) { await bind.sessionChangeResolution( id: widget.id, width: w, height: h); + Future.delayed(Duration(seconds: 3), () async { + final display = widget.ffi.ffiModel.display; + if (w == display.width && h == display.height) { + if (_isWindowCanBeAdjusted()) { + _doAdjustWindow(); + } + } + }); } } } From 920477bbb2d9fde761fc1c1321ea19bca7d58912 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 13:13:51 +0800 Subject: [PATCH 18/39] delete discovery from RustDesk_lan_peers.toml Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 8 ++++++-- src/flutter_ffi.rs | 4 ++++ src/ui.rs | 4 +--- src/ui_interface.rs | 7 +++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8d4d58772..470b631ce 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -534,7 +534,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { () async { if (isLan) { - // TODO + bind.mainRemoveDiscovered(id: id); } else { final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { @@ -859,7 +859,11 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); + menuItems.add( + _removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + }, isLan: true), + ); return menuItems; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8a8bf4de4..23a65c2da 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -796,6 +796,10 @@ pub fn main_load_lan_peers() { }; } +pub fn main_remove_discovered(id: String) { + remove_discovered(id); +} + fn main_broadcast_message(data: &HashMap<&str, &str>) { let apps = vec![ flutter::APP_TYPE_DESKTOP_REMOTE, diff --git a/src/ui.rs b/src/ui.rs index 1b6838e46..a197cb257 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -413,9 +413,7 @@ impl UI { } fn remove_discovered(&mut self, id: String) { - let mut peers = config::LanPeers::load().peers; - peers.retain(|x| x.id != id); - config::LanPeers::store(&peers); + remove_discovered(id); } fn send_wol(&mut self, id: String) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dd111f86e..3b2ba0897 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -596,6 +596,13 @@ pub fn get_lan_peers() -> Vec> { .collect() } +#[inline] +pub fn remove_discovered(id: String) { + let mut peers = config::LanPeers::load().peers; + peers.retain(|x| x.id != id); + config::LanPeers::store(&peers); +} + #[inline] pub fn get_uuid() -> String { base64::encode(hbb_common::get_uuid()) From 0ad6bca9ceb87bcdd17354a29460ac34aec3fef5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 13:39:28 +0800 Subject: [PATCH 19/39] check existence for visibility of addFavAction/addToAb Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 38 ++++++++++------------- src/flutter_ffi.rs | 4 +++ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 470b631ce..5b6120a02 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -745,12 +745,9 @@ class RecentPeerCard extends BasePeerCard { } if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); @@ -797,12 +794,9 @@ class FavoritePeerCard extends BasePeerCard { })); if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); @@ -843,19 +837,19 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } - if (!favs.contains(peer.id)) { - menuItems.add(_addFavAction(peer.id)); - } else { - menuItems.add(_rmFavAction(peer.id, () async {})); + final inRecent = await bind.mainIsInRecentPeers(id: peer.id); + if (inRecent) { + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } } if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 23a65c2da..0866ff739 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -726,6 +726,10 @@ pub fn main_peer_has_password(id: String) -> bool { peer_has_password(id) } +pub fn main_is_in_recent_peers(id: String) -> bool { + PeerConfig::peers().iter().any(|e| e.0 == id) +} + pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec> = PeerConfig::peers() From a9598e006a61a6eeee758c0031025b9a50eca0a1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 15:51:13 +0800 Subject: [PATCH 20/39] request elevation menu Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 10 ++++++++++ flutter/lib/mobile/widgets/dialog.dart | 7 +++---- flutter/lib/models/model.dart | 20 +++++++++++++++++++ src/client/io_loop.rs | 19 ++++++++++-------- src/flutter.rs | 7 +++++++ src/server/connection.rs | 16 +++++---------- src/ui/remote.rs | 2 ++ src/ui_session_interface.rs | 1 + 8 files changed, 59 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4f9a227bd..bdf9c1f1d 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -598,6 +598,7 @@ class _ControlMenu extends StatelessWidget { hoverColor: _MenubarTheme.hoverBlueColor, ffi: ffi, menuChildren: [ + requestElevation(), osPassword(), transferFile(context), tcpTunneling(context), @@ -611,6 +612,15 @@ class _ControlMenu extends StatelessWidget { ]); } + requestElevation() { + final visible = ffi.elevationModel.showRequestMenu; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Request Elevation')), + ffi: ffi, + onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)); + } + osPassword() { return _MenuItemButton( child: Text(translate('OS Password')), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 7e9a9879c..931999382 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -374,8 +374,7 @@ void showWaitUacDialog( )); } -void _showRequestElevationDialog( - String id, OverlayDialogManager dialogManager) { +void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) { RxString groupValue = ''.obs; RxString errUser = ''.obs; RxString errPwd = ''.obs; @@ -531,7 +530,7 @@ void showOnBlockDialog( dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); - _showRequestElevationDialog(id, dialogManager); + showRequestElevationDialog(id, dialogManager); } return CustomAlertDialog( @@ -553,7 +552,7 @@ void showElevationError(String id, String type, String title, String text, dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); - _showRequestElevationDialog(id, dialogManager); + showRequestElevationDialog(id, dialogManager); } return CustomAlertDialog( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f4efe2f08..eae41679f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,8 @@ class FfiModel with ChangeNotifier { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); closeConnection(id: peer_id); + } else if (name == 'portable_service_running') { + parent.target?.elevationModel.onPortableServiceRunning(evt); } else if (name == "on_url_scheme_received") { final url = evt['url'].toString(); parseRustdeskUri(url); @@ -439,6 +441,7 @@ class FfiModel with ChangeNotifier { Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; handleResolutions(peerId, evt["resolutions"]); + parent.target?.elevationModel.onPeerInfo(_pi); } notifyListeners(); } @@ -1395,6 +1398,21 @@ class RecordingModel with ChangeNotifier { } } +class ElevationModel with ChangeNotifier { + WeakReference parent; + ElevationModel(this.parent); + bool _running = false; + bool _canElevate = false; + bool get showRequestMenu => _canElevate && !_running; + onPeerInfo(PeerInfo pi) { + _canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false; + } + + onPortableServiceRunning(Map evt) { + _running = evt['running'] == 'true'; + } +} + enum ConnType { defaultConn, fileTransfer, portForward, rdp } /// Flutter state manager and data communication with the Rust core. @@ -1420,6 +1438,7 @@ class FFI { late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // session late final InputModel inputModel; // session + late final ElevationModel elevationModel; // session FFI() { imageModel = ImageModel(WeakReference(this)); @@ -1436,6 +1455,7 @@ class FFI { qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); + elevationModel = ElevationModel(WeakReference(this)); } /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b51c481a5..1c7788193 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -56,6 +56,7 @@ pub struct Remote { data_count: Arc, frame_count: Arc, video_format: CodecFormat, + elevation_requested: bool, } impl Remote { @@ -87,6 +88,7 @@ impl Remote { video_format: CodecFormat::Unknown, stop_voice_call_sender: None, voice_call_request_timestamp: None, + elevation_requested: false, } } @@ -686,6 +688,7 @@ impl Remote { let mut msg = Message::new(); msg.set_misc(misc); allow_err!(peer.send(&msg).await); + self.elevation_requested = true; } Data::ElevateWithLogon(username, password) => { let mut request = ElevationRequest::new(); @@ -699,6 +702,7 @@ impl Remote { let mut msg = Message::new(); msg.set_misc(misc); allow_err!(peer.send(&msg).await); + self.elevation_requested = true; } Data::NewVoiceCall => { let msg = new_voice_call_request(true); @@ -1181,7 +1185,8 @@ impl Remote { } } Some(misc::Union::PortableServiceRunning(b)) => { - if b { + self.handler.portable_service_running(b); + if self.elevation_requested && b { self.handler.msgbox( "custom-nocancel-success", "Successful", @@ -1253,14 +1258,12 @@ impl Remote { } } } - Some(message::Union::PeerInfo(pi)) => { - match pi.conn_id { - crate::SYNC_PEER_INFO_DISPLAYS => { - self.handler.set_displays(&pi.displays); - } - _ => {} + Some(message::Union::PeerInfo(pi)) => match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); } - } + _ => {} + }, _ => {} } } diff --git a/src/flutter.rs b/src/flutter.rs index ea73eb925..2f660775f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -572,6 +572,13 @@ impl InvokeUiSession for FlutterHandler { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + fn portable_service_running(&self, running: bool) { + self.push_event( + "portable_service_running", + [("running", running.to_string().as_str())].into(), + ); + } + fn on_voice_call_started(&self) { self.push_event("on_voice_call_started", [].into()); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 85fcb676b..b2e198681 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1552,7 +1552,6 @@ impl Connection { .err() .map_or("".to_string(), |e| e.to_string()); } - self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1571,7 +1570,6 @@ impl Connection { .err() .map_or("".to_string(), |e| e.to_string()); } - self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1936,13 +1934,11 @@ impl Connection { let p = &mut self.portable; if running != p.last_running { p.last_running = running; - if running && p.elevation_requested { - let mut misc = Misc::new(); - misc.set_portable_service_running(running); - let mut msg = Message::new(); - msg.set_misc(misc); - self.inner.send(msg.into()); - } + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if p.last_uac != uac { @@ -2166,7 +2162,6 @@ pub struct PortableState { pub last_foreground_window_elevated: bool, pub last_running: bool, pub is_installed: bool, - pub elevation_requested: bool, } #[cfg(windows)] @@ -2177,7 +2172,6 @@ impl Default for PortableState { last_uac: Default::default(), last_foreground_window_elevated: Default::default(), last_running: Default::default(), - elevation_requested: Default::default(), } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 7b31c84e9..c6e0229b2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -277,6 +277,8 @@ impl InvokeUiSession for SciterHandler { fn switch_back(&self, _id: &str) {} + fn portable_service_running(&self, _running: bool) {} + fn on_voice_call_started(&self) { self.call("onVoiceCallStart", &make_args!()); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index fd5a7d9c0..f726ed526 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -805,6 +805,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn portable_service_running(&self, running: bool); fn on_voice_call_started(&self); fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); From c3c4505132b3e7109361d02a1b6fa69a7377bd9b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 14:15:54 +0800 Subject: [PATCH 21/39] feat: make file manager draggable --- flutter/lib/consts.dart | 2 + .../lib/desktop/pages/file_manager_page.dart | 170 ++++++++++++------ .../lib/desktop/widgets/dragable_divider.dart | 53 ++++++ .../widgets/list_search_action_listener.dart | 1 + 4 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 flutter/lib/desktop/widgets/dragable_divider.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b73182fd..537784918 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -53,6 +53,8 @@ const int kDesktopMaxDisplayHeight = 1080; const double kDesktopFileTransferNameColWidth = 200; const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferMinimumWidth = 100; +const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0d55552af..68023f929 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; @@ -78,6 +79,10 @@ class _FileManagerPageState extends State final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote"); final _listSearchBufferLocal = TimeoutStringBuffer(); final _listSearchBufferRemote = TimeoutStringBuffer(); + final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs; + final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs; /// [_lastClickTime], [_lastClickEntry] help to handle double click int _lastClickTime = @@ -297,11 +302,12 @@ class _FileManagerPageState extends State } var searchResult = entries .skip(skipCount) - .where((element) => element.name.startsWith(buffer)); + .where((element) => element.name.toLowerCase().startsWith(buffer)); if (searchResult.isEmpty) { // cannot find next, lets restart search from head + debugPrint("restart search from head"); searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); } if (searchResult.isEmpty) { setState(() { @@ -316,7 +322,7 @@ class _FileManagerPageState extends State debugPrint("searching for $buffer"); final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); selectedEntries.clear(); if (searchResult.isEmpty) { setState(() { @@ -362,37 +368,41 @@ class _FileManagerPageState extends State child: Row( children: [ GestureDetector( - child: Container( - width: kDesktopFileTransferNameColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : SvgPicture.asset( - entry.isFile - ? "assets/file.svg" - : "assets/folder.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, - ), - Expanded( - child: Text( - entry.name.nonBreaking, - overflow: - TextOverflow.ellipsis)) - ]), - )), + child: Obx( + () => Container( + width: isLocal + ? _nameColWidthLocal.value + : _nameColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + Expanded( + child: Text( + entry.name.nonBreaking, + overflow: + TextOverflow.ellipsis)) + ]), + )), + ), onTap: () { final items = getSelectedItems(isLocal); // handle double click @@ -406,24 +416,35 @@ class _FileManagerPageState extends State items, filteredEntries, entry, isLocal); }, ), + SizedBox( + width: 2.0, + ), GestureDetector( - child: SizedBox( - width: kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - )), + child: Obx( + () => SizedBox( + width: isLocal + ? _modifiedColWidthLocal.value + : _modifiedColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), + ), ), ), + // Divider from header. SizedBox( - width: 100, + width: 2.0, + ), + Expanded( + // width: 100, child: GestureDetector( child: Tooltip( waitDuration: Duration(milliseconds: 500), @@ -1362,6 +1383,7 @@ class _FileManagerPageState extends State Text( name, style: headerTextStyle, + overflow: TextOverflow.ellipsis, ).marginSymmetric(horizontal: 4), ascending.value != null ? Icon( @@ -1383,16 +1405,48 @@ class _FileManagerPageState extends State } Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) { - return Row( - children: [ - headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name, - translate("Name"), isLocal), - headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified, - translate("Modified"), isLocal), - Expanded( - child: - headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) - ], + final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote; + final modifiedColWidth = + isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote; + final padding = EdgeInsets.all(1.0); + return SizedBox( + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Obx( + () => headerItemFunc( + nameColWidth.value, SortBy.name, translate("Name"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + nameColWidth.value += dx; + nameColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + nameColWidth.value)); + }, + padding: padding, + ), + Obx( + () => headerItemFunc(modifiedColWidth.value, SortBy.modified, + translate("Modified"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + modifiedColWidth.value += dx; + modifiedColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + modifiedColWidth.value)); + }, + padding: padding), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ), ); } } diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart new file mode 100644 index 000000000..3821b7e0d --- /dev/null +++ b/flutter/lib/desktop/widgets/dragable_divider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class DraggableDivider extends StatefulWidget { + final Axis axis; + final double thickness; + final Color color; + final Function(double)? onPointerMove; + final VoidCallback? onHover; + final EdgeInsets padding; + const DraggableDivider({ + super.key, + this.axis = Axis.horizontal, + this.thickness = 1.0, + this.color = const Color.fromARGB(200, 177, 175, 175), + this.onPointerMove, + this.padding = const EdgeInsets.symmetric(horizontal: 1.0), + this.onHover, + }); + + @override + State createState() => _DraggableDividerState(); +} + +class _DraggableDividerState extends State { + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (event) { + final dl = + widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx; + widget.onPointerMove?.call(dl); + }, + onPointerHover: (event) => widget.onHover?.call(), + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: Padding( + padding: widget.padding, + child: Container( + decoration: BoxDecoration(color: widget.color), + width: widget.axis == Axis.horizontal + ? double.infinity + : widget.thickness, + height: widget.axis == Axis.horizontal + ? widget.thickness + : double.infinity, + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart index 9598c3400..36128bf26 100644 --- a/flutter/lib/desktop/widgets/list_search_action_listener.dart +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -55,6 +55,7 @@ class TimeoutStringBuffer { } ListSearchAction input(String ch) { + ch = ch.toLowerCase(); final curr = DateTime.now(); try { if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { From b10c0ffe54c30649a7e3394a7ccf1f03295cd59d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 15:56:37 +0800 Subject: [PATCH 22/39] opt: fs explorer resizable & search next for loop --- .../lib/desktop/pages/file_manager_page.dart | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 68023f929..569e1cb9a 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -316,7 +316,7 @@ class _FileManagerPageState extends State return; } _jumpToEntry(isLocal, searchResult.first, scrollController, - kDesktopFileTransferRowHeight, buffer); + kDesktopFileTransferRowHeight); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -331,7 +331,7 @@ class _FileManagerPageState extends State return; } _jumpToEntry(isLocal, searchResult.first, scrollController, - kDesktopFileTransferRowHeight, buffer); + kDesktopFileTransferRowHeight); }, child: ObxValue( (searchText) { @@ -471,7 +471,11 @@ class _FileManagerPageState extends State return Column( children: [ // Header - _buildFileBrowserHeader(context, isLocal), + Row( + children: [ + Expanded(child: _buildFileBrowserHeader(context, isLocal)), + ], + ), // Body Expanded( child: ListView.builder( @@ -493,7 +497,7 @@ class _FileManagerPageState extends State } void _jumpToEntry(bool isLocal, Entry entry, - ScrollController scrollController, double rowHeight, String buffer) { + ScrollController scrollController, double rowHeight) { final entries = model.getCurrentDir(isLocal).entries; final index = entries.indexOf(entry); if (index == -1) { @@ -501,7 +505,7 @@ class _FileManagerPageState extends State } final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element == entry); selectedEntries.clear(); if (searchResult.isEmpty) { return; @@ -1380,18 +1384,23 @@ class _FileManagerPageState extends State height: kDesktopFileTransferHeaderHeight, child: Row( children: [ - Text( - name, - style: headerTextStyle, - overflow: TextOverflow.ellipsis, - ).marginSymmetric(horizontal: 4), - ascending.value != null + Flexible( + flex: 2, + child: Text( + name, + style: headerTextStyle, + overflow: TextOverflow.ellipsis, + ).marginSymmetric(horizontal: 4), + ), + Flexible( + flex: 1, + child: ascending.value != null ? Icon( ascending.value! ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, ) - : const Offstage() + : const Offstage()) ], ), ), From 47a514a41670b2a4f9134219bf2854dd412c0d0f Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 16:20:00 +0800 Subject: [PATCH 23/39] optimize menubar code Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index bdf9c1f1d..8710d27c3 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1101,7 +1101,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { await bind.sessionSetImageQuality(id: widget.id, value: value); } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, child: Text(translate('Image Quality')), menuChildren: [ _RadioMenuButton( @@ -1135,7 +1136,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { }, ffi: widget.ffi, ), - ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), + ], ); }); } @@ -1310,7 +1311,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { bind.sessionChangePreferCodec(id: widget.id); } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, child: Text(translate('Codec')), menuChildren: [ _RadioMenuButton( @@ -1341,7 +1343,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChanged: onChanged, ffi: widget.ffi, ), - ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); + ]); }); } @@ -1373,7 +1375,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, menuChildren: resolutions .map((e) => _RadioMenuButton( value: '${e.width}x${e.height}', @@ -1381,8 +1384,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChanged: onChanged, ffi: widget.ffi, child: Text('${e.width}x${e.height}'))) - .toList() - .map((e) => _buildPointerTrackWidget(e, widget.ffi)) .toList(), child: Text(translate("Resolution"))); } @@ -1869,6 +1870,28 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> { } } +class _SubmenuButton extends StatelessWidget { + final List menuChildren; + final Widget? child; + final FFI ffi; + const _SubmenuButton({ + Key? key, + required this.menuChildren, + required this.child, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubmenuButton( + key: key, + child: child, + menuChildren: + menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(), + ); + } +} + class _MenuItemButton extends StatelessWidget { final VoidCallback? onPressed; final Widget? trailingIcon; From 2a71b65a618364aaf9f30f75fc428a2fc07f8940 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 19:06:37 +0800 Subject: [PATCH 24/39] add missing insertLock menu Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 8710d27c3..37bbbd66a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -606,6 +606,7 @@ class _ControlMenu extends StatelessWidget { Divider(), ctrlAltDel(), restart(), + insertLock(), blockUserInput(), switchSides(), refresh(), @@ -789,6 +790,16 @@ class _ControlMenu extends StatelessWidget { onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); } + insertLock() { + final perms = ffi.ffiModel.permissions; + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Insert Lock')), + ffi: ffi, + onPressed: () => bind.sessionLockScreen(id: id)); + } + blockUserInput() { final perms = ffi.ffiModel.permissions; final pi = ffi.ffiModel.pi; From c6f8df36a2018ea3a20cb692bf439ea989c6a6cc Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 19:55:46 +0800 Subject: [PATCH 25/39] update options after login Signed-off-by: fufesou --- src/server/connection.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index b2e198681..898939b62 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -126,6 +126,7 @@ pub struct Connection { origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, + options_in_login: Option, } impl ConnInner { @@ -233,6 +234,7 @@ impl Connection { audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, + options_in_login: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -921,6 +923,9 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; + if let Some(o) = self.options_in_login.take() { + self.update_options(&o).await; + } if let Some((dir, show_hidden)) = self.file_transfer.clone() { let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { &dir @@ -1106,8 +1111,7 @@ impl Connection { async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { - // It may not be a good practice to update all options here. - self.update_options(o).await; + self.options_in_login = Some(o.clone()); if let Some(q) = o.video_codec_state.clone().take() { scrap::codec::Encoder::update_video_encoder( self.inner.id(), @@ -1697,7 +1701,8 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } - async fn update_options_without_auth(&mut self, o: &OptionMessage) { + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { @@ -1728,12 +1733,6 @@ impl Connection { scrap::codec::EncoderUpdate::State(q), ); } - } - - async fn update_options_with_auth(&mut self, o: &OptionMessage) { - if !self.authorized { - return; - } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; @@ -1862,12 +1861,6 @@ impl Connection { } } - async fn update_options(&mut self, o: &OptionMessage) { - log::info!("Option update: {:?}", o); - self.update_options_without_auth(o).await; - self.update_options_with_auth(o).await; - } - async fn on_close(&mut self, reason: &str, lock: bool) { log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { From 1e387ce01917c7951f2b88dcb59c08bd59627cbf Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 20:24:31 +0800 Subject: [PATCH 26/39] simple refact Signed-off-by: fufesou --- src/platform/linux.rs | 120 +++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 08e343d49..47184e796 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,14 +1,22 @@ use super::{CursorData, ResultType}; -use hbb_common::libc::{c_char, c_int, c_long, c_void}; pub use hbb_common::platform::linux::*; -use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, + libc::{c_char, c_int, c_long, c_void}, + log, + message_proto::Resolution, +}; use std::{ cell::RefCell, - path::PathBuf, + path::{Path, PathBuf}, + process::{Child, Command}, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, + time::{Duration, Instant}, }; use xrandr_parser::Parser; @@ -162,10 +170,29 @@ fn start_uinput_service() { }); } -fn stop_server(server: &mut Option) { +#[inline] +fn try_start_server_(user: Option<(String, String)>) -> ResultType> { + if user.is_some() { + run_as_user(vec!["--server"], user) + } else { + Ok(Some(crate::run_me(vec!["--server"])?)) + } +} + +#[inline] +fn start_server(user: Option<(String, String)>, server: &mut Option) { + match try_start_server_(user) { + Ok(ps) => *server = ps, + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } +} + +fn stop_server(server: &mut Option) { if let Some(mut ps) = server.take() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); + std::thread::sleep(Duration::from_millis(30)); match ps.try_wait() { Ok(Some(_status)) => {} Ok(None) => { @@ -182,7 +209,7 @@ fn set_x11_env(uid: &str) { let mut auth = get_env_tries("XAUTHORITY", uid, 10); // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 if auth.is_empty() || uid == "0" { - auth = if std::path::Path::new(&gdm).exists() { + auth = if Path::new(&gdm).exists() { gdm } else { let username = get_active_username(); @@ -190,7 +217,7 @@ fn set_x11_env(uid: &str) { format!("/{}/.Xauthority", username) } else { let tmp = format!("/home/{}/.Xauthority", username); - if std::path::Path::new(&tmp).exists() { + if Path::new(&tmp).exists() { tmp } else { format!("/var/lib/{}/.Xauthority", username) @@ -223,8 +250,8 @@ fn should_start_server( uid: &mut String, cur_uid: String, cm0: &mut bool, - last_restart: &mut std::time::Instant, - server: &mut Option, + last_restart: &mut Instant, + server: &mut Option, ) -> bool { let cm = get_cm(); let mut start_new = false; @@ -235,8 +262,8 @@ fn should_start_server( } if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - *last_restart = std::time::Instant::now(); + std::thread::sleep(Duration::from_millis(30)); + *last_restart = Instant::now(); } } else if !cm && ((*cm0 && last_restart.elapsed().as_secs() > 60) @@ -247,8 +274,8 @@ fn should_start_server( // and x server get displays failure issue if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - *last_restart = std::time::Instant::now(); + std::thread::sleep(Duration::from_millis(30)); + *last_restart = Instant::now(); log::info!("restart server"); } } @@ -267,6 +294,13 @@ fn should_start_server( start_new } +// to-do: stop_server(&mut user_server); may not stop child correctly +// stop_rustdesk_servers() is just a temp solution here. +fn force_stop_server() { + stop_rustdesk_servers(); + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); +} + pub fn start_os_service() { stop_rustdesk_servers(); start_uinput_service(); @@ -274,8 +308,8 @@ pub fn start_os_service() { let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let mut uid = "".to_owned(); - let mut server: Option = None; - let mut user_server: Option = None; + let mut server: Option = None; + let mut user_server: Option = None; if let Err(err) = ctrlc::set_handler(move || { r.store(false, Ordering::SeqCst); }) { @@ -283,12 +317,13 @@ pub fn start_os_service() { } let mut cm0 = false; - let mut last_restart = std::time::Instant::now(); + let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { let (cur_uid, cur_user) = get_active_user_id_name(); let is_wayland = current_is_wayland(); if cur_user == "root" || !is_wayland { + // try kill subprocess "--server" stop_server(&mut user_server); // try start subprocess "--server" if should_start_server( @@ -299,16 +334,8 @@ pub fn start_os_service() { &mut last_restart, &mut server, ) { - // to-do: stop_server(&mut user_server); may not stop child correctly - // stop_rustdesk_servers() is just a temp solution here. - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match crate::run_me(vec!["--server"]) { - Ok(ps) => server = Some(ps), - Err(err) => { - log::error!("Failed to start server: {}", err); - } - } + force_stop_server(); + start_server(None, &mut server); } } else if cur_user != "" { if cur_user != "gdm" { @@ -324,23 +351,16 @@ pub fn start_os_service() { &mut last_restart, &mut user_server, ) { - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match run_as_user(vec!["--server"], Some((cur_uid, cur_user))) { - Ok(ps) => user_server = ps, - Err(err) => { - log::error!("Failed to start server: {}", err); - } - } + force_stop_server(); + start_server(Some((cur_uid, cur_user)), &mut user_server); } } } else { - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + force_stop_server(); stop_server(&mut user_server); stop_server(&mut server); } - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); } if let Some(ps) = user_server.take().as_mut() { @@ -362,7 +382,7 @@ pub fn get_active_userid() -> String { } fn get_cm() -> bool { - if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() { + if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains(&format!( "{} --cm", @@ -380,7 +400,7 @@ fn get_cm() -> bool { fn get_display() -> String { let user = get_active_username(); log::debug!("w {}", &user); - if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + if let Ok(output) = Command::new("w").arg(&user).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { log::debug!(" {}", line); let mut iter = line.split_whitespace(); @@ -395,7 +415,7 @@ fn get_display() -> String { // above not work for gdm user log::debug!("ls -l /tmp/.X11-unix/"); let mut last = "".to_owned(); - if let Ok(output) = std::process::Command::new("ls") + if let Ok(output) = Command::new("ls") .args(vec!["-l", "/tmp/.X11-unix/"]) .output() { @@ -474,10 +494,7 @@ fn is_opensuse() -> bool { false } -pub fn run_as_user( - arg: Vec<&str>, - user: Option<(String, String)>, -) -> ResultType> { +pub fn run_as_user(arg: Vec<&str>, user: Option<(String, String)>) -> ResultType> { let (uid, username) = match user { Some(id_name) => id_name, None => get_active_user_id_name(), @@ -491,7 +508,7 @@ pub fn run_as_user( args.insert(0, "-E"); } - let task = std::process::Command::new("sudo").args(args).spawn()?; + let task = Command::new("sudo").args(args).spawn()?; Ok(Some(task)) } @@ -553,10 +570,7 @@ pub fn get_default_pa_source() -> Option<(String, String)> { } pub fn lock_screen() { - std::process::Command::new("xdg-screensaver") - .arg("lock") - .spawn() - .ok(); + Command::new("xdg-screensaver").arg("lock").spawn().ok(); } pub fn toggle_blank_screen(_v: bool) { @@ -577,7 +591,7 @@ fn get_env_tries(name: &str, uid: &str, n: usize) -> String { if !x.is_empty() { return x; } - std::thread::sleep(std::time::Duration::from_millis(300)); + std::thread::sleep(Duration::from_millis(300)); } "".to_owned() } @@ -604,12 +618,12 @@ pub fn quit_gui() { pub fn check_super_user_permission() -> ResultType { let file = "/usr/share/rustdesk/files/polkit"; let arg; - if std::path::Path::new(file).is_file() { + if Path::new(file).is_file() { arg = file; } else { arg = "echo"; } - let status = std::process::Command::new("pkexec").arg(arg).status()?; + let status = Command::new("pkexec").arg(arg).status()?; Ok(status.success() && status.code() == Some(0)) } @@ -684,7 +698,7 @@ pub fn current_resolution(name: &str) -> ResultType { } pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { - std::process::Command::new("xrandr") + Command::new("xrandr") .args(vec![ "--output", name, From 8d726f53aabf33652bb8efaa190bb24ca7093a28 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 23:38:23 +0800 Subject: [PATCH 27/39] better mouse position Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 9a5b06b14..fca73eac7 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -459,17 +459,22 @@ class InputModel { } evt['type'] = type; if (isDesktop) { - y = y - stateGlobal.tabBarHeight; + y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value; + x -= stateGlobal.windowBorderWidth.value; } final canvasModel = parent.target!.canvasModel; + final nearThr = 3; + var nearRight = (canvasModel.size.width - x) < nearThr; + var nearBottom = (canvasModel.size.height - y) < nearThr; + final ffiModel = parent.target!.ffiModel; if (isMove) { canvasModel.moveDesktopMouse(x, y); } final d = ffiModel.display; + final imageWidth = d.width * canvasModel.scale; + final imageHeight = d.height * canvasModel.scale; if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { - final imageWidth = d.width * canvasModel.scale; - final imageHeight = d.height * canvasModel.scale; x += imageWidth * canvasModel.scrollX; y += imageHeight * canvasModel.scrollY; @@ -487,6 +492,15 @@ class InputModel { x /= canvasModel.scale; y /= canvasModel.scale; + if (canvasModel.scale > 0 && canvasModel.scale < 1) { + final step = 1.0 / canvasModel.scale - 1; + if (nearRight) { + x += step; + } + if (nearBottom) { + y += step; + } + } x += d.x; y += d.y; From ac2c7115344e1065cfc77efc8d6f516a2b660b58 Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Fri, 24 Feb 2023 17:04:05 +0100 Subject: [PATCH 28/39] Update it.rs --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 7256b13d8..04b2488a7 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -454,7 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), ("Reconnect", "Riconnetti"), - ("Codec", ""), - ("Resolution", ""), + ("Codec", "Codec"), + ("Resolution", "Risoluzione"), ].iter().cloned().collect(); } From 0a51fff04c984355efcc30a1d5130d0174a591cc Mon Sep 17 00:00:00 2001 From: Michal Witek Date: Fri, 24 Feb 2023 17:19:33 +0100 Subject: [PATCH 29/39] Added additional intent-filter --- flutter/android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 9b25f4973..ede6353ef 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:exported="false"> + From 218ce22fbd6f7d0d1c4acc9f03a10bb5419914e6 Mon Sep 17 00:00:00 2001 From: Integral <71180087+Integral-Tech@users.noreply.github.com> Date: Sat, 25 Feb 2023 14:25:35 +0800 Subject: [PATCH 30/39] Update cn.rs Some small fixes --- src/lang/cn.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 818e63203..fd2e04c53 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -38,7 +38,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "停止服务"), ("Change ID", "更改 ID"), ("Your new ID", "你的新 ID"), - ("length %min% to %max%", "长度在 %min 与 %max 之间"), + ("length %min% to %max%", "长度在 %min% 与 %max% 之间"), ("starts with a letter", "以字母开头"), ("allowed characters", "使用允许的字符"), ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), @@ -137,7 +137,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), ("Remote desktop is offline", "远程电脑处于离线状态"), - ("Key mismatch", "密钥不匹配"), + ("Key mismatch", "Key 不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), From f71d4e9e815edf2486f01158250ef50873f5f1b4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 25 Feb 2023 18:39:27 +0800 Subject: [PATCH 31/39] initialize vdi with .devcontainer --- vdi/README.md | 1 + vdi/host/.devcontainer/Dockerfile | 16 + vdi/host/.devcontainer/devcontainer.json | 27 + vdi/host/.gitignore | 1 + vdi/host/Cargo.lock | 1185 ++++++++++++++++++++++ vdi/host/Cargo.toml | 9 + vdi/host/README.md | 1 + vdi/host/src/main.rs | 2 + 8 files changed, 1242 insertions(+) create mode 100644 vdi/README.md create mode 100644 vdi/host/.devcontainer/Dockerfile create mode 100644 vdi/host/.devcontainer/devcontainer.json create mode 100644 vdi/host/.gitignore create mode 100644 vdi/host/Cargo.lock create mode 100644 vdi/host/Cargo.toml create mode 100644 vdi/host/README.md create mode 100644 vdi/host/src/main.rs diff --git a/vdi/README.md b/vdi/README.md new file mode 100644 index 000000000..85e6ff194 --- /dev/null +++ b/vdi/README.md @@ -0,0 +1 @@ +# WIP diff --git a/vdi/host/.devcontainer/Dockerfile b/vdi/host/.devcontainer/Dockerfile new file mode 100644 index 000000000..f02042771 --- /dev/null +++ b/vdi/host/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +FROM rockylinux:9.1 +ENV HOME=/home/vscode +ENV WORKDIR=$HOME/rustdesk/vdi/host + +# https://ciq.co/blog/top-10-things-to-do-after-rocky-linux-9-install/ also gpu driver install +WORKDIR $HOME +RUN dnf -y install epel-release +RUN dnf config-manager --set-enabled crb +RUN dnf -y install cargo libvpx-devel opus-devel usbredir-devel git cmake gcc-c++ pkg-config nasm yasm ninja-build automake libtool libva-devel libvdpau-devel llvm-devel +WORKDIR / + +RUN git clone https://chromium.googlesource.com/libyuv/libyuv +WORKDIR /libyuv +RUN cmake . -DCMAKE_INSTALL_PREFIX=/user +RUN make -j4 && make install +WORKDIR / \ No newline at end of file diff --git a/vdi/host/.devcontainer/devcontainer.json b/vdi/host/.devcontainer/devcontainer.json new file mode 100644 index 000000000..02c6e589b --- /dev/null +++ b/vdi/host/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +{ + "name": "rustdesk", + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk", + "customizations": { + "vscode": { + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates", + "mhutchie.git-graph", + "eamodio.gitlens" + ], + "settings": { + "files.watcherExclude": { + "**/target/**": true + } + } + } + } +} \ No newline at end of file diff --git a/vdi/host/.gitignore b/vdi/host/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/vdi/host/.gitignore @@ -0,0 +1 @@ +/target diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock new file mode 100644 index 000000000..d4254717e --- /dev/null +++ b/vdi/host/Cargo.lock @@ -0,0 +1,1185 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-broadcast" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" +dependencies = [ + "easy-parallel", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "easy-parallel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" + +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "ordered-stream" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qemu-display" +version = "0.1.0" +source = "git+https://gitlab.com/marcandre.lureau/qemu-display#544a4075615702abf414cd2d63bbb6a9ca10d0ea" +dependencies = [ + "async-broadcast 0.3.4", + "async-lock", + "async-trait", + "cfg-if", + "derivative", + "enumflags2", + "futures", + "futures-util", + "libc", + "log", + "once_cell", + "serde", + "serde_bytes", + "serde_repr", + "uds_windows", + "usbredirhost", + "windows", + "zbus", + "zvariant", +] + +[[package]] +name = "qemu-rustdesk" +version = "0.1.0" +dependencies = [ + "qemu-display", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusb" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703aa035c21c589b34fb5136b12e68fc8dcf7ea46486861381361dd8ebf5cee0" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_repr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" + +[[package]] +name = "toml_edit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +dependencies = [ + "indexmap", + "nom8", + "toml_datetime", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "usbredirhost" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87485e4dfeb0176203afd1086f11ed2ead837053143b12b6eed55c598e9393d5" +dependencies = [ + "libc", + "rusb", + "usbredirhost-sys", + "usbredirparser", +] + +[[package]] +name = "usbredirhost-sys" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b27c305da1f7601b665d68948bcfaf9909d443bec94510ab776118ab8afc2c7d" +dependencies = [ + "libusb1-sys", + "pkg-config", + "usbredirparser-sys", +] + +[[package]] +name = "usbredirparser" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f8b5241d7cbb3e08b4677212a9ac001f116f50731c2737d16129a84ecf6a56" +dependencies = [ + "libc", + "usbredirparser-sys", +] + +[[package]] +name = "usbredirparser-sys" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b0e834e187916fc762bccdc9d64e454a0ee58b134f8f7adab321141e8e0d91" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zbus" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ce2de393c874ba871292e881bf3c13a0d5eb38170ebab2e50b4c410eaa222b" +dependencies = [ + "async-broadcast 0.4.1", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde-xml-rs", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13d08f5dc6cf725b693cb6ceacd43cd430ec0664a879188f29e7d7dcd98f96d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zvariant" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "serde_bytes", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml new file mode 100644 index 000000000..62c964122 --- /dev/null +++ b/vdi/host/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "qemu-rustdesk" +version = "0.1.0" +authors = ["rustdesk "] +edition = "2021" + + +[dependencies] +qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } \ No newline at end of file diff --git a/vdi/host/README.md b/vdi/host/README.md new file mode 100644 index 000000000..3b29a10e3 --- /dev/null +++ b/vdi/host/README.md @@ -0,0 +1 @@ +# RustDesk protocol on QEMU D-Bus display diff --git a/vdi/host/src/main.rs b/vdi/host/src/main.rs new file mode 100644 index 000000000..f79c691f0 --- /dev/null +++ b/vdi/host/src/main.rs @@ -0,0 +1,2 @@ +fn main() { +} From b8b3e996026fa47352feb9bcb30c45cc5b4a1442 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 22:39:12 +0800 Subject: [PATCH 32/39] macos, periodically check if current display is changed Signed-off-by: fufesou --- src/platform/macos.mm | 3 +-- src/platform/macos.rs | 8 ++++---- src/server/video_service.rs | 8 ++++++++ src/ui.rs | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 443351469..8be0c6db5 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -60,7 +60,7 @@ extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_ return false; } *numModes = CFArrayGetCount(allModes); - for (int i = 0; i < *numModes && i < max; i++) { + for (uint32_t i = 0; i < *numModes && i < max; i++) { CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); widths[i] = (uint32_t)CGDisplayModeGetWidth(mode); heights[i] = (uint32_t)CGDisplayModeGetHeight(mode); @@ -136,7 +136,6 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h return ret; } int numModes = CFArrayGetCount(allModes); - CGDisplayModeRef bestMode = NULL; for (int i = 0; i < numModes; i++) { CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); if (width == CGDisplayModeGetWidth(mode) && diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 025274840..b663b0f41 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -612,18 +612,18 @@ pub fn resolutions(name: &str) -> Vec { unsafe { if YES == MacGetModeNum(display, &mut num) { let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); - let mut realNum = 0; + let mut real_num = 0; if YES == MacGetModes( display, widths.as_mut_ptr(), heights.as_mut_ptr(), num, - &mut realNum, + &mut real_num, ) { - if realNum <= num { - for i in 0..realNum { + if real_num <= num { + for i in 0..real_num { let resolution = Resolution { width: widths[i as usize] as _, height: heights[i as usize] as _, diff --git a/src/server/video_service.rs b/src/server/video_service.rs index a9a9fd9ab..affb5eb17 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -577,6 +577,14 @@ fn run(sp: GenericService) -> ResultType<()> { if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; + // Capturer on macos does not return Err event the solution is changed. + #[cfg(target_os = "macos")] + if check_display_changed(c.ndisplay, c.current, c.width, c.height) { + log::info!("Displays changed"); + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + if let Some(msg_out) = check_displays_changed() { sp.send(msg_out); } diff --git a/src/ui.rs b/src/ui.rs index a197cb257..e0449fd95 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, LocalConfig, PeerConfig}, + config::{LocalConfig, PeerConfig}, log, }; From 59cd775d5fe7606891ebbcff80f329510b52bff8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 22:47:22 +0800 Subject: [PATCH 33/39] fix notify peer resolution change Signed-off-by: fufesou --- flutter/lib/models/model.dart | 79 +++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index eae41679f..e48d74dac 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -156,7 +156,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt, peerId); + updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ''); @@ -241,36 +241,33 @@ class FfiModel with ChangeNotifier { } } - handleSwitchDisplay(Map evt, String peerId) { - final oldOrientation = _display.width > _display.height; - var old = _pi.currentDisplay; - _pi.currentDisplay = int.parse(evt['display']); - _display.x = double.parse(evt['x']); - _display.y = double.parse(evt['y']); - _display.width = int.parse(evt['width']); - _display.height = int.parse(evt['height']); - _display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; - if (old != _pi.currentDisplay) { - parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + _updateCurDisplay(String peerId, Display newDisplay) { + if (newDisplay != _display) { + if (newDisplay.x != _display.x || newDisplay.y != _display.y) { + parent.target?.cursorModel + .updateDisplayOrigin(newDisplay.x, newDisplay.y); + } + _display = newDisplay; + _updateSessionWidthHeight(peerId); } + } - _updateSessionWidthHeight(peerId, display.width, display.height); + handleSwitchDisplay(Map evt, String peerId) { + _pi.currentDisplay = int.parse(evt['display']); + var newDisplay = Display(); + newDisplay.x = double.parse(evt['x']); + newDisplay.y = double.parse(evt['y']); + newDisplay.width = int.parse(evt['width']); + newDisplay.height = int.parse(evt['height']); + newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; + + _updateCurDisplay(peerId, newDisplay); try { CurrentDisplayState.find(peerId).value = _pi.currentDisplay; } catch (e) { // } - - // remote is mobile, and orientation changed - if ((_display.width > _display.height) != oldOrientation) { - gFFI.canvasModel.updateViewStyle(); - } - if (_pi.platform == kPeerPlatformLinux || - _pi.platform == kPeerPlatformWindows || - _pi.platform == kPeerPlatformMacOS) { - parent.target?.canvasModel.updateViewStyle(); - } parent.target?.recordingModel.onSwitchDisplay(); handleResolutions(peerId, evt["resolutions"]); notifyListeners(); @@ -372,7 +369,8 @@ class FfiModel with ChangeNotifier { }); } - _updateSessionWidthHeight(String id, int width, int height) { + _updateSessionWidthHeight(String id) { + parent.target?.canvasModel.updateViewStyle(); bind.sessionSetSize(id: id, width: display.width, height: display.height); } @@ -429,7 +427,7 @@ class FfiModel with ChangeNotifier { stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; - _updateSessionWidthHeight(peerId, display.width, display.height); + _updateSessionWidthHeight(peerId); } if (displays.isNotEmpty) { parent.target?.dialogManager.showLoading( @@ -488,7 +486,7 @@ class FfiModel with ChangeNotifier { _pi.displays = newDisplays; stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { - _display = _pi.displays[_pi.currentDisplay]; + _updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]); } } notifyListeners(); @@ -797,12 +795,18 @@ class CanvasModel with ChangeNotifier { final dh = getDisplayHeight() * _scale; var dxOffset = 0; var dyOffset = 0; - if (dw > size.width) { - dxOffset = (x - dw * (x / size.width) - _x).toInt(); - } - if (dh > size.height) { - dyOffset = (y - dh * (y / size.height) - _y).toInt(); + try { + if (dw > size.width) { + dxOffset = (x - dw * (x / size.width) - _x).toInt(); + } + if (dh > size.height) { + dyOffset = (y - dh * (y / size.height) - _y).toInt(); + } + } catch (e) { + // Unhandled Exception: Unsupported operation: Infinity or NaN toInt + return; } + _x += dxOffset; _y += dyOffset; if (dxOffset != 0 || dyOffset != 0) { @@ -1579,6 +1583,19 @@ class Display { ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; } + + @override + bool operator ==(Object other) => + other is Display && + other.runtimeType == runtimeType && + _innerEqual(other); + + bool _innerEqual(Display other) => + other.x == x && + other.y == y && + other.width == width && + other.height == height && + other.cursorEmbedded == cursorEmbedded; } class Resolution { From e876adaec58ed45761a0aae250425d49b75faebd Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 22:59:59 +0800 Subject: [PATCH 34/39] fix, flutter keyboard grab, connecting peers of older version Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 37bbbd66a..8500bec03 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -23,6 +23,10 @@ import '../../common/shared_state.dart'; import './popup_menu.dart'; import './kb_layout_type_chooser.dart'; +const _kKeyLegacyMode = 'legacy'; +const _kKeyMapMode = 'map'; +const _kKeyTranslateMode = 'translate'; + class MenubarState { final kStoreKey = 'remoteMenubarState'; late RxBool show; @@ -1540,9 +1544,10 @@ class _KeyboardMenu extends StatelessWidget { Widget build(BuildContext context) { var ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) return Offstage(); - // Do not support peer 1.1.9. if (stateGlobal.grabKeyboard) { - bind.sessionSetKeyboardMode(id: id, value: 'map'); + if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { + bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); + } return Offstage(); } return _IconSubmenuButton( @@ -1555,13 +1560,13 @@ class _KeyboardMenu extends StatelessWidget { mode() { return futureBuilder(future: () async { - return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy'; + return await bind.sessionGetKeyboardMode(id: id) ?? _kKeyLegacyMode; }(), hasData: (data) { final groupValue = data as String; List modes = [ - KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), - KeyboardModeMenu(key: 'map', menu: 'Map mode'), - KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), + KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), + KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), ]; List<_RadioMenuButton> list = []; onChanged(String? value) async { @@ -1571,13 +1576,13 @@ class _KeyboardMenu extends StatelessWidget { for (KeyboardModeMenu mode in modes) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { - if (mode.key == 'translate') { + if (mode.key == _kKeyTranslateMode) { if (Platform.isLinux || pi.platform == kPeerPlatformLinux) { continue; } } var text = translate(mode.menu); - if (mode.key == 'translate') { + if (mode.key == _kKeyTranslateMode) { text = '$text beta'; } list.add(_RadioMenuButton( From 6740587d8adf6a8cc62c6d09ead26a1a878400c2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 25 Feb 2023 23:11:28 +0800 Subject: [PATCH 35/39] do not check keyboard permission on menu build Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 8500bec03..c27546d9f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1542,11 +1542,15 @@ class _KeyboardMenu extends StatelessWidget { @override Widget build(BuildContext context) { - var ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) return Offstage(); + // Do not check permission here? + // var ffiModel = Provider.of(context); + // if (ffiModel.permissions['keyboard'] == false) return Offstage(); if (stateGlobal.grabKeyboard) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); + } else if (bind.sessionIsKeyboardModeSupported( + id: id, mode: _kKeyLegacyMode)) { + bind.sessionSetKeyboardMode(id: id, value: _kKeyLegacyMode); } return Offstage(); } From 037641624c82aa5cd0cf6ae8475bb9ef82b4172a Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Sat, 25 Feb 2023 21:39:21 +0100 Subject: [PATCH 36/39] Update es.rs New terms added --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index d7e43b6bf..5ae368581 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -454,7 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Detener llamada de voz"), ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), ("Reconnect", "Reconectar"), - ("Codec", ""), - ("Resolution", ""), + ("Codec", "Códec"), + ("Resolution", "Resolución"), ].iter().cloned().collect(); } From 8092f3ad94f11112d8c37b60d45dfd928be1eeb1 Mon Sep 17 00:00:00 2001 From: PCKUO Date: Sun, 26 Feb 2023 12:51:16 +0800 Subject: [PATCH 37/39] Make up for the missing --- src/lang/tw.rs | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ca1193eaa..e20c7f254 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -37,19 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "剪貼簿是空的"), ("Stop service", "停止服務"), ("Change ID", "更改 ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "你的新 ID"), + ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), + ("starts with a letter", "以字母開頭"), + ("allowed characters", "使用允許的字元"), ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Website", "網站"), ("About", "關於"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隱私聲明"), ("Mute", "靜音"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "建構日期"), + ("Version", "版本"), + ("Home", "主頁"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), ("Hardware Codec", "硬件編解碼"), @@ -213,15 +213,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "由對方手動關閉"), ("Enable remote configuration modification", "啟用遠端更改設定"), ("Run without install", "跳過安裝直接執行"), - ("Connect via relay", ""), + ("Connect via relay", "中繼連線"), ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "驗證"), + ("Remember me", "記住我"), + ("Trust this device", "信任此設備"), + ("Verification code", "驗證碼"), + ("verification_tip", "檢測到新設備登錄,已向註冊郵箱發送了登入驗證碼,請輸入驗證碼繼續登錄"), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -391,12 +391,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Show RustDesk", "顯示 RustDesk"), + ("This PC", "此電腦"), + ("or", "或"), + ("Continue with", "使用"), ("Elevate", "提權"), - ("Zoom cursor", ""), + ("Zoom cursor", "縮放游標"), ("Accept sessions via password", "只允許密碼訪問"), ("Accept sessions via click", "只允許點擊訪問"), ("Accept sessions via both", "允許密碼或點擊訪問"), @@ -407,9 +407,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "請求訪問你的設備"), ("Hide connection management window", "隱藏連接管理窗口"), ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), - ("wayland_experiment_tip", ""), + ("wayland_experiment_tip", "Wayland 支持處於實驗階段,如果你需要使用無人值守訪問,請使用 X11。"), ("Right click to select tabs", "右鍵選擇選項卡"), - ("Skipped", ""), + ("Skipped", "已略過"), ("Add to Address Book", "添加到地址簿"), ("Group", "小組"), ("Search", "搜索"), @@ -418,8 +418,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "請選擇本地鍵盤類型"), ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), - ("config_input", ""), - ("config_microphone", ""), + ("config_input", "為了能夠通過鍵盤控制遠程桌面, 請給予 RustDesk \"輸入監控\" 權限。"), + ("config_microphone", "為了支持通過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "提權失敗"), @@ -438,8 +438,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "弱"), ("Medium", "中"), ("Strong", "強"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "反轉訪問方向"), + ("Please confirm if you want to share your desktop?", "請確認是否要讓對方訪問你的桌面?"), ("Display", "顯示"), ("Default View Style", "默認顯示方式"), ("Default Scroll Style", "默認滾動方式"), From 4b2529125567207721489caaf2085532d998705c Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 26 Feb 2023 11:23:43 +0800 Subject: [PATCH 38/39] sciter/mobile id suffix "\r" or "/r" for relay Signed-off-by: 21pages --- flutter/lib/common.dart | 6 ++++-- .../lib/desktop/pages/connection_page.dart | 5 +---- src/flutter_ffi.rs | 4 ++++ src/ui.rs | 19 ++++++++++++++----- src/ui/index.tis | 5 ++++- src/ui/remote.rs | 3 ++- src/ui_interface.rs | 9 +++++++++ 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ff373cc9c..e89671711 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1453,10 +1453,12 @@ connectMainDesktop(String id, connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, - bool isRDP = false, - bool forceRelay = false}) async { + bool isRDP = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); + final oldId = id; + id = await bind.mainHandleRelayId(id: id); + final forceRelay = id != oldId; assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 4aad66eee..edbd5b7c6 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -151,10 +151,7 @@ class _ConnectionPageState extends State /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { var id = _idController.id; - var forceRelay = id.endsWith(r'/r'); - if (forceRelay) id = id.substring(0, id.length - 2); - connect(context, id, - isFileTransfer: isFileTransfer, forceRelay: forceRelay); + connect(context, id, isFileTransfer: isFileTransfer); } /// UI for the remote ID TextField. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0866ff739..e49ba65f7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -840,6 +840,10 @@ pub fn main_get_user_default_option(key: String) -> SyncReturn { SyncReturn(get_user_default_option(key)) } +pub fn main_handle_relay_id(id: String) -> String { + handle_relay_id(id) +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/ui.rs b/src/ui.rs index e0449fd95..22c44ec5f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -420,8 +420,8 @@ impl UI { crate::lan::send_wol(id) } - fn new_remote(&mut self, id: String, remote_type: String) { - new_remote(id, remote_type) + fn new_remote(&mut self, id: String, remote_type: String, force_relay: bool) { + new_remote(id, remote_type, force_relay) } fn is_process_trusted(&mut self, _prompt: bool) -> bool { @@ -571,6 +571,10 @@ impl UI { fn default_video_save_directory(&self) -> String { default_video_save_directory() } + + fn handle_relay_id(&self, id: String) -> String { + handle_relay_id(id) + } } impl sciter::EventHandler for UI { @@ -588,7 +592,7 @@ impl sciter::EventHandler for UI { fn set_remote_id(String); fn closing(i32, i32, i32, i32); fn get_size(); - fn new_remote(String, bool); + fn new_remote(String, String, bool); fn send_wol(String); fn remove_peer(String); fn remove_discovered(String); @@ -653,6 +657,7 @@ impl sciter::EventHandler for UI { fn has_hwcodec(); fn get_langs(); fn default_video_save_directory(); + fn handle_relay_id(String); } } @@ -718,9 +723,13 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc> { } #[inline] -pub fn new_remote(id: String, remote_type: String) { +pub fn new_remote(id: String, remote_type: String, force_relay: bool) { let mut lock = CHILDREN.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; + let mut args = vec![format!("--{}", remote_type), id.clone()]; + if force_relay { + args.push("".to_string()); // password + args.push("--relay".to_string()); + } let key = (id.clone(), remote_type.clone()); if let Some(c) = lock.1.get_mut(&key) { if let Ok(Some(_)) = c.try_wait() { diff --git a/src/ui/index.tis b/src/ui/index.tis index ec2e0a748..0e2247070 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -62,12 +62,15 @@ function createNewConnect(id, type) { id = id.replace(/\s/g, ""); app.remote_id.value = formatId(id); if (!id) return; + var old_id = id; + id = handler.handle_relay_id(id); + var force_relay = old_id != id; if (id == my_id) { msgbox("custom-error", "Error", "You cannot connect to your own computer"); return; } handler.set_remote_id(id); - handler.new_remote(id, type); + handler.new_remote(id, type, force_relay); } class ShareRdp: Reactor.Component { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index c6e0229b2..ed16f1e0e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -462,6 +462,7 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { + let force_relay = args.contains(&"--relay".to_string()); let session: Session = Session { id: id.clone(), password: password.clone(), @@ -486,7 +487,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, false); + .initialize(id, conn_type, None, force_relay); Self(session) } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 3b2ba0897..62eba25c1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -970,3 +970,12 @@ async fn check_id( } "" } + +// if it's relay id, return id processed, otherwise return original id +pub fn handle_relay_id(id: String) -> String { + if id.ends_with(r"\r") || id.ends_with(r"/r") { + id[0..id.len() - 2].to_string() + } else { + id + } +} From 8fa487e5b07f1c53b6cfd79fbda952e22e2bbcea Mon Sep 17 00:00:00 2001 From: Michal Witek Date: Sun, 26 Feb 2023 09:07:00 +0100 Subject: [PATCH 39/39] Added missing translation --- src/lang/pl.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index eea46accb..fe6454fad 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -284,13 +284,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Akceptujesz?"), ("Open System Setting", "Otwórz ustawienia systemowe"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), - ("android_input_permission_tip1", "android_input_permission_tip1"), - ("android_input_permission_tip2", "android_input_permission_tip2"), - ("android_new_connection_tip", "android_new_connection_tip"), - ("android_service_will_start_tip", "android_service_will_start_tip"), - ("android_stop_service_tip", "android_stop_service_tip"), - ("android_version_audio_tip", "android_version_audio_tip"), - ("android_start_service_tip", "android_start_service_tip"), + ("android_input_permission_tip1", "Aby można było sterować Twoim urządzeniem za pomocą myszy lub dotyku, musisz zezwolić RustDesk na korzystanie z usługi \"Ułatwienia dostępu\"."), + ("android_input_permission_tip2", "Przejdź do następnej strony ustawień systemowych, znajdź i wejdź w [Zainstalowane usługi], włącz usługę [RustDesk Input]."), + ("android_new_connection_tip", "Otrzymano nowe żądanie zdalnego dostępu, które chce przejąć kontrolę nad Twoim urządzeniem."), + ("android_service_will_start_tip", "Włączenie opcji „Przechwytywanie ekranu” spowoduje automatyczne uruchomienie usługi, umożliwiając innym urządzeniom żądanie połączenia z Twoim urządzeniem."), + ("android_stop_service_tip", "Zamknięcie usługi spowoduje automatyczne zamknięcie wszystkich nawiązanych połączeń."), + ("android_version_audio_tip", "Bieżąca wersja systemu Android nie obsługuje przechwytywania dźwięku, zaktualizuj system do wersji Android 10 lub nowszej."), + ("android_start_service_tip", "Kliknij [Uruchom usługę] lub Otwórz [Przechwytywanie ekranu], aby uruchomić usługę udostępniania ekranu."), ("Account", "Konto"), ("Overwrite", "Nadpisz"), ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"), @@ -311,7 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Język"), ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), - ("android_open_battery_optimizations_tip", "android_open_battery_optimizations_tip"), + ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), ("Connection not allowed", "Połączenie niedozwolone"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), @@ -449,12 +449,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), + ("Voice call", "Rozmowa głosowa"), + ("Text chat", "Chat tekstowy"), + ("Stop voice call", "Rozłącz"), + ("relay_hint_tip", "Bezpośrednie połączenie może nie być możliwe, możesz spróbować połączyć się przez serwer przekazujący. \nDodatkowo, jeśli chcesz użyć serwera przekazującego przy pierwszej próbie, możesz dodać sufiks \"/r\" do identyfikatora lub wybrać opcję \"Zawsze łącz przez serwer przekazujący\" na karcie peer-ów."), + ("Reconnect", "Połącz ponownie"), + ("Codec", "Kodek"), + ("Resolution", "Rozdzielczość"), + ("Use temporary password", "Użyj hasła tymczasowego"), + ("Set temporary password length", "Ustaw długość hasła tymczasowego"), + ("Key", "Klucz") ].iter().cloned().collect(); }