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/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">
+
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index e1b9ac90c..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");
@@ -1819,3 +1821,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/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index 0a175139f..a69fc3bbe 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -515,15 +515,31 @@ 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('Delete'),
+ 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 {
if (isLan) {
- // TODO
+ bind.mainRemoveDiscovered(id: id);
} else {
+ final favs = (await bind.mainGetFav()).toList();
+ if (favs.remove(id)) {
+ await bind.mainStoreFav(favs: favs);
+ }
await bind.mainRemovePeer(id: id);
}
removePreference(id);
@@ -553,9 +569,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 {
@@ -575,9 +603,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 {
@@ -642,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')),
),
),
),
@@ -677,6 +718,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));
}
@@ -690,16 +734,26 @@ 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));
}
- menuItems.add(_addFavAction(peer.id));
- if (!gFFI.abModel.idContainBy(peer.id)) {
- menuItems.add(_addToAb(peer));
+
+ 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));
+ }
+ }
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {
+ await bind.mainLoadRecentPeers();
+ }));
return menuItems;
}
@@ -732,18 +786,23 @@ 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));
}
menuItems.add(_rmFavAction(peer.id, () async {
await bind.mainLoadFavPeers();
}));
- if (!gFFI.abModel.idContainBy(peer.id)) {
- menuItems.add(_addToAb(peer));
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ if (!gFFI.abModel.idContainBy(peer.id)) {
+ menuItems.add(_addToAb(peer));
+ }
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {
+ await bind.mainLoadFavPeers();
+ }));
return menuItems;
}
@@ -763,6 +822,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));
}
@@ -774,11 +836,28 @@ class DiscoveredPeerCard extends BasePeerCard {
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
- menuItems.add(MenuEntryDivider());
- menuItems.add(_removeAction(peer.id, () async {}));
- if (!gFFI.abModel.idContainBy(peer.id)) {
- menuItems.add(_addToAb(peer));
+
+ 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));
+ }
+ }
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(
+ _removeAction(peer.id, () async {
+ await bind.mainLoadLanPeers();
+ }, isLan: true),
+ );
return menuItems;
}
@@ -811,13 +890,15 @@ 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(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {}));
return menuItems;
}
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/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/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index 06a79093a..e041b591d 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/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index c3322fef7..badb68a84 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(() {
@@ -310,13 +316,13 @@ class _FileManagerPageState extends State
return;
}
_jumpToEntry(isLocal, searchResult.first, scrollController,
- kDesktopFileTransferRowHeight, buffer);
+ kDesktopFileTransferRowHeight);
},
onSearch: (buffer) {
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(() {
@@ -325,7 +331,7 @@ class _FileManagerPageState extends State
return;
}
_jumpToEntry(isLocal, searchResult.first, scrollController,
- kDesktopFileTransferRowHeight, buffer);
+ kDesktopFileTransferRowHeight);
},
child: ObxValue(
(searchText) {
@@ -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),
@@ -450,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(
@@ -472,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) {
@@ -480,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;
@@ -1396,17 +1421,23 @@ class _FileManagerPageState extends State
height: kDesktopFileTransferHeaderHeight,
child: Row(
children: [
- Text(
- name,
- style: headerTextStyle,
- ).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())
],
),
),
@@ -1420,16 +1451,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) {
diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index 45857aa45..c27546d9f 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,9 +21,12 @@ 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';
+const _kKeyLegacyMode = 'legacy';
+const _kKeyMapMode = 'map';
+const _kKeyTranslateMode = 'translate';
+
class MenubarState {
final kStoreKey = 'remoteMenubarState';
late RxBool show;
@@ -102,6 +103,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 +286,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 +302,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 +352,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 +379,612 @@ 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: 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 {
+ 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: [
+ requestElevation(),
+ osPassword(),
+ transferFile(context),
+ tcpTunneling(context),
+ note(),
+ Divider(),
+ ctrlAltDel(),
+ restart(),
+ insertLock(),
+ blockUserInput(),
+ switchSides(),
+ refresh(),
+ ]);
+ }
+
+ 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')),
+ 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));
+ }
+
+ 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;
+ 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 +1005,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 +1035,278 @@ 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(
+ ffi: widget.ffi,
+ 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,
+ groupValue: groupValue,
+ onChanged: (value) {
+ onChanged(value);
+ _customImageQualityDialog();
+ },
+ ffi: widget.ffi,
),
],
- 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();
- },
- 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,
- ),
- );
+ _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 +1314,690 @@ 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(
+ ffi: widget.ffi,
+ 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,
+ ),
+ ]);
+ });
+ }
-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);
+ Future.delayed(Duration(seconds: 3), () async {
+ final display = widget.ffi.ffiModel.display;
+ if (w == display.width && h == display.height) {
+ if (_isWindowCanBeAdjusted()) {
+ _doAdjustWindow();
+ }
+ }
+ });
+ }
+ }
}
- 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(
+ ffi: widget.ffi,
+ 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(),
+ 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) {
+ // 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();
+ }
+ 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) ?? _kKeyLegacyMode;
+ }(), hasData: (data) {
+ final groupValue = data as String;
+ List modes = [
+ 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 {
+ 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 == _kKeyTranslateMode) {
+ if (Platform.isLinux || pi.platform == kPeerPlatformLinux) {
+ continue;
+ }
+ }
+ var text = translate(mode.menu);
+ if (mode.key == _kKeyTranslateMode) {
+ 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 _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;
+ 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 +2097,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/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/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;
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 5ef72a0af..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'] ?? '');
@@ -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);
@@ -239,37 +241,35 @@ 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();
}
@@ -369,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);
}
@@ -426,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(
@@ -437,10 +438,36 @@ 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();
}
+ 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 +485,9 @@ class FfiModel with ChangeNotifier {
}
_pi.displays = newDisplays;
stateGlobal.displaysCount.value = _pi.displays.length;
+ if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) {
+ _updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]);
+ }
}
notifyListeners();
}
@@ -765,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) {
@@ -1366,6 +1402,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.
@@ -1391,6 +1442,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));
@@ -1407,6 +1459,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].
@@ -1530,6 +1583,30 @@ 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 {
+ int width = 0;
+ int height = 0;
+ Resolution(this.width, this.height);
+
+ @override
+ String toString() {
+ return 'Resolution($width,$height)';
+ }
}
class Features {
@@ -1545,6 +1622,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/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/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/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);
}
diff --git a/src/flutter.rs b/src/flutter.rs
index d366a0eda..2f660775f 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),
],
);
}
@@ -568,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());
}
@@ -861,6 +872,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..e49ba65f7 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);
@@ -720,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()
@@ -790,6 +800,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,
@@ -826,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/lang/ca.rs b/src/lang/ca.rs
index 4a6536afa..aa33ae6e5 100644
--- a/src/lang/ca.rs
+++ b/src/lang/ca.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/cn.rs b/src/lang/cn.rs
index 593e023d2..f975e343f 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", "无法通过注册服务器建立连接"),
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "停止语音通话"),
("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"),
("Reconnect", "重连"),
+ ("Codec", "编解码"),
+ ("Resolution", "分辨率"),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/cs.rs b/src/lang/cs.rs
index d09bba096..cfe69924c 100644
--- a/src/lang/cs.rs
+++ b/src/lang/cs.rs
@@ -455,5 +455,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("relay_hint_tip", ""),
("Reconnect", ""),
("No transfers in progress", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/da.rs b/src/lang/da.rs
index 428150eff..19310357b 100644
--- a/src/lang/da.rs
+++ b/src/lang/da.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/de.rs b/src/lang/de.rs
index 8bfa973c4..ee28fe0e7 100644
--- a/src/lang/de.rs
+++ b/src/lang/de.rs
@@ -454,6 +454,8 @@ 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", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/eo.rs b/src/lang/eo.rs
index 1ea05d5ab..9b7912cff 100644
--- a/src/lang/eo.rs
+++ b/src/lang/eo.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/es.rs b/src/lang/es.rs
index 494dd4cb7..af0da0479 100644
--- a/src/lang/es.rs
+++ b/src/lang/es.rs
@@ -454,6 +454,8 @@ 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", "Códec"),
+ ("Resolution", "Resolución"),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/fa.rs b/src/lang/fa.rs
index 72dd93a10..0c31e1531 100644
--- a/src/lang/fa.rs
+++ b/src/lang/fa.rs
@@ -455,5 +455,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر میخواهید فوراً از سرور رله استفاده کنید، میتوانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"),
("Reconnect", "اتصال مجدد"),
("No transfers in progress", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/fr.rs b/src/lang/fr.rs
index a600c92eb..0e45827f7 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"),
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/gr.rs b/src/lang/gr.rs
index 26e5a649c..fca98f228 100644
--- a/src/lang/gr.rs
+++ b/src/lang/gr.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/hu.rs b/src/lang/hu.rs
index 125d982dc..437cf445a 100644
--- a/src/lang/hu.rs
+++ b/src/lang/hu.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/id.rs b/src/lang/id.rs
index 17a9589a8..84892a7f8 100644
--- a/src/lang/id.rs
+++ b/src/lang/id.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/it.rs b/src/lang/it.rs
index 0749603b0..101685c4a 100644
--- a/src/lang/it.rs
+++ b/src/lang/it.rs
@@ -455,5 +455,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("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"),
("No transfers in progress", "Nessun trasferimento in corso"),
+ ("Codec", "Codec"),
+ ("Resolution", "Risoluzione"),
].iter().cloned().collect();
}
diff --git a/src/lang/ja.rs b/src/lang/ja.rs
index 7c810cbac..c19b607ca 100644
--- a/src/lang/ja.rs
+++ b/src/lang/ja.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/ko.rs b/src/lang/ko.rs
index 87425b402..97574e67d 100644
--- a/src/lang/ko.rs
+++ b/src/lang/ko.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/kz.rs b/src/lang/kz.rs
index 49017f4ff..54a51b439 100644
--- a/src/lang/kz.rs
+++ b/src/lang/kz.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/nl.rs b/src/lang/nl.rs
index 615c3601b..eb7c214ab 100644
--- a/src/lang/nl.rs
+++ b/src/lang/nl.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Stop spraakoproep"),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/pl.rs b/src/lang/pl.rs
index ae1743ee2..5269c4ee8 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,11 +449,16 @@ 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", ""),
+ ("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")
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs
index 4e619a6e4..923bbab05 100644
--- a/src/lang/pt_PT.rs
+++ b/src/lang/pt_PT.rs
@@ -455,5 +455,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("relay_hint_tip", ""),
("Reconnect", ""),
("No transfers in progress", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs
index 8f4b2b13d..aa491f951 100644
--- a/src/lang/ptbr.rs
+++ b/src/lang/ptbr.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/ro.rs b/src/lang/ro.rs
index ebefd1a80..e992b19d8 100644
--- a/src/lang/ro.rs
+++ b/src/lang/ro.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/ru.rs b/src/lang/ru.rs
index d652904cc..07b8af998 100644
--- a/src/lang/ru.rs
+++ b/src/lang/ru.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "Завершить голосовой вызов"),
("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."),
("Reconnect", "Переподключить"),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/sk.rs b/src/lang/sk.rs
index 47843fd55..6468b7eef 100644
--- a/src/lang/sk.rs
+++ b/src/lang/sk.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/sl.rs b/src/lang/sl.rs
index 95d65a453..d128e7322 100755
--- a/src/lang/sl.rs
+++ b/src/lang/sl.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/sq.rs b/src/lang/sq.rs
index 3ec67d77a..29c5cbbf8 100644
--- a/src/lang/sq.rs
+++ b/src/lang/sq.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/sr.rs b/src/lang/sr.rs
index 31f408748..63173dc11 100644
--- a/src/lang/sr.rs
+++ b/src/lang/sr.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/sv.rs b/src/lang/sv.rs
index 3afcf1a9a..1a00ece43 100644
--- a/src/lang/sv.rs
+++ b/src/lang/sv.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/template.rs b/src/lang/template.rs
index 53df7b4f7..2c83f9474 100644
--- a/src/lang/template.rs
+++ b/src/lang/template.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/th.rs b/src/lang/th.rs
index a5db9708c..6fcf02ed2 100644
--- a/src/lang/th.rs
+++ b/src/lang/th.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/tr.rs b/src/lang/tr.rs
index 96e35e2b1..d35d288d6 100644
--- a/src/lang/tr.rs
+++ b/src/lang/tr.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/tw.rs b/src/lang/tw.rs
index f49e81a7b..20a2998ec 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", "默認滾動方式"),
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", "停止語音聊天"),
("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"),
("Reconnect", "重連"),
+ ("Codec", "編解碼"),
+ ("Resolution", "分辨率"),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/ua.rs b/src/lang/ua.rs
index 1729d42a7..4c4b5d4bc 100644
--- a/src/lang/ua.rs
+++ b/src/lang/ua.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/lang/vn.rs b/src/lang/vn.rs
index 5521022b7..32cd084cb 100644
--- a/src/lang/vn.rs
+++ b/src/lang/vn.rs
@@ -454,6 +454,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop voice call", ""),
("relay_hint_tip", ""),
("Reconnect", ""),
+ ("Codec", ""),
+ ("Resolution", ""),
("No transfers in progress", ""),
].iter().cloned().collect();
}
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index 32c32efb9..47184e796 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -1,15 +1,24 @@
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,
+ 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;
type Xdo = *const c_void;
@@ -161,10 +170,29 @@ fn start_uinput_service() {
});
}
-fn stop_server(server: &mut Option) {
+#[inline]
+fn try_start_server_(user: Option<(String, String)>) -> ResultType