Merge branch 'master'

This commit is contained in:
NicKoehler 2023-02-26 10:12:52 +01:00
commit 0a3b5f10a6
No known key found for this signature in database
GPG Key ID: BAE01394EB51AC58
77 changed files with 4121 additions and 1618 deletions

72
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -26,6 +26,7 @@
android:exported="false">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

View File

@ -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();
}
});
}

View File

@ -515,15 +515,31 @@ abstract class BasePeerCard extends StatelessWidget {
String id, Future<void> Function() reloadFunc,
{bool isLan = false}) {
return MenuEntryButton<String>(
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<String> _addFavAction(String id) {
return MenuEntryButton<String>(
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<String> _rmFavAction(
String id, Future<void> Function() reloadFunc) {
return MenuEntryButton<String>(
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;
}

View File

@ -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;

View File

@ -151,10 +151,7 @@ class _ConnectionPageState extends State<ConnectionPage>
/// 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.

View File

@ -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<String> 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,

View File

@ -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<FileManagerPage>
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<FileManagerPage>
}
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<FileManagerPage>
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<FileManagerPage>
return;
}
_jumpToEntry(isLocal, searchResult.first, scrollController,
kDesktopFileTransferRowHeight, buffer);
kDesktopFileTransferRowHeight);
},
child: ObxValue<RxString>(
(searchText) {
@ -362,37 +368,41 @@ class _FileManagerPageState extends State<FileManagerPage>
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<FileManagerPage>
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<FileManagerPage>
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<FileManagerPage>
}
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<FileManagerPage>
}
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<FileManagerPage>
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<FileManagerPage>
}
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))
],
),
);
}
}

View File

@ -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<DraggableDivider> createState() => _DraggableDividerState();
}
class _DraggableDividerState extends State<DraggableDivider> {
@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,
),
),
),
);
}
}

View File

@ -55,6 +55,7 @@ class TimeoutStringBuffer {
}
ListSearchAction input(String ch) {
ch = ch.toLowerCase();
final curr = DateTime.now();
try {
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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;

View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<dynamic> dynamicArray = jsonDecode(resolutions as String);
List<Resolution> 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<String, dynamic> 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<FFI> 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<String, dynamic> 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<Display> displays = [];
Features features = Features();
List<Resolution> resolutions = [];
}
const canvasKey = 'canvas';

View File

@ -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;
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -56,6 +56,7 @@ pub struct Remote<T: InvokeUiSession> {
data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>,
video_format: CodecFormat,
elevation_requested: bool,
}
impl<T: InvokeUiSession> Remote<T> {
@ -87,6 +88,7 @@ impl<T: InvokeUiSession> Remote<T> {
video_format: CodecFormat::Unknown,
stop_voice_call_sender: None,
voice_call_request_timestamp: None,
elevation_requested: false,
}
}
@ -686,6 +688,7 @@ impl<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
}
}
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<T: InvokeUiSession> Remote<T> {
}
}
}
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);
}
}
_ => {}
},
_ => {}
}
}

View File

@ -52,6 +52,11 @@ lazy_static::lazy_static! {
pub static ref DEVICE_NAME: Arc<Mutex<String>> = Default::default();
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
lazy_static::lazy_static! {
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
}
pub fn global_init() -> bool {
#[cfg(target_os = "linux")]
{
@ -96,7 +101,11 @@ pub fn check_clipboard(
) -> Option<Message> {
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<Mutex<String>>>)
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);
}

View File

@ -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<Resolution>) -> 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 {

View File

@ -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<HashMap<&str, String>> = 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<String> {
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,

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<std::process::Child>) {
#[inline]
fn try_start_server_(user: Option<(String, String)>) -> ResultType<Option<Child>> {
if user.is_some() {
run_as_user(vec!["--server"], user)
} else {
Ok(Some(crate::run_me(vec!["--server"])?))
}
}
#[inline]
fn start_server(user: Option<(String, String)>, server: &mut Option<Child>) {
match try_start_server_(user) {
Ok(ps) => *server = ps,
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
}
fn stop_server(server: &mut Option<Child>) {
if let Some(mut ps) = server.take() {
allow_err!(ps.kill());
std::thread::sleep(std::time::Duration::from_millis(30));
std::thread::sleep(Duration::from_millis(30));
match ps.try_wait() {
Ok(Some(_status)) => {}
Ok(None) => {
@ -181,7 +209,7 @@ fn set_x11_env(uid: &str) {
let mut auth = get_env_tries("XAUTHORITY", uid, 10);
// auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468
if auth.is_empty() || uid == "0" {
auth = if std::path::Path::new(&gdm).exists() {
auth = if Path::new(&gdm).exists() {
gdm
} else {
let username = get_active_username();
@ -189,7 +217,7 @@ fn set_x11_env(uid: &str) {
format!("/{}/.Xauthority", username)
} else {
let tmp = format!("/home/{}/.Xauthority", username);
if std::path::Path::new(&tmp).exists() {
if Path::new(&tmp).exists() {
tmp
} else {
format!("/var/lib/{}/.Xauthority", username)
@ -222,8 +250,8 @@ fn should_start_server(
uid: &mut String,
cur_uid: String,
cm0: &mut bool,
last_restart: &mut std::time::Instant,
server: &mut Option<std::process::Child>,
last_restart: &mut Instant,
server: &mut Option<Child>,
) -> bool {
let cm = get_cm();
let mut start_new = false;
@ -234,8 +262,8 @@ fn should_start_server(
}
if let Some(ps) = server.as_mut() {
allow_err!(ps.kill());
std::thread::sleep(std::time::Duration::from_millis(30));
*last_restart = std::time::Instant::now();
std::thread::sleep(Duration::from_millis(30));
*last_restart = Instant::now();
}
} else if !cm
&& ((*cm0 && last_restart.elapsed().as_secs() > 60)
@ -246,8 +274,8 @@ fn should_start_server(
// and x server get displays failure issue
if let Some(ps) = server.as_mut() {
allow_err!(ps.kill());
std::thread::sleep(std::time::Duration::from_millis(30));
*last_restart = std::time::Instant::now();
std::thread::sleep(Duration::from_millis(30));
*last_restart = Instant::now();
log::info!("restart server");
}
}
@ -266,6 +294,13 @@ fn should_start_server(
start_new
}
// to-do: stop_server(&mut user_server); may not stop child correctly
// stop_rustdesk_servers() is just a temp solution here.
fn force_stop_server() {
stop_rustdesk_servers();
std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL));
}
pub fn start_os_service() {
stop_rustdesk_servers();
start_uinput_service();
@ -273,8 +308,8 @@ pub fn start_os_service() {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let mut uid = "".to_owned();
let mut server: Option<std::process::Child> = None;
let mut user_server: Option<std::process::Child> = None;
let mut server: Option<Child> = None;
let mut user_server: Option<Child> = None;
if let Err(err) = ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
}) {
@ -282,12 +317,13 @@ pub fn start_os_service() {
}
let mut cm0 = false;
let mut last_restart = std::time::Instant::now();
let mut last_restart = Instant::now();
while running.load(Ordering::SeqCst) {
let (cur_uid, cur_user) = get_active_user_id_name();
let is_wayland = current_is_wayland();
if cur_user == "root" || !is_wayland {
// try kill subprocess "--server"
stop_server(&mut user_server);
// try start subprocess "--server"
if should_start_server(
@ -298,16 +334,8 @@ pub fn start_os_service() {
&mut last_restart,
&mut server,
) {
// to-do: stop_server(&mut user_server); may not stop child correctly
// stop_rustdesk_servers() is just a temp solution here.
stop_rustdesk_servers();
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
match crate::run_me(vec!["--server"]) {
Ok(ps) => server = Some(ps),
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
force_stop_server();
start_server(None, &mut server);
}
} else if cur_user != "" {
if cur_user != "gdm" {
@ -323,23 +351,16 @@ pub fn start_os_service() {
&mut last_restart,
&mut user_server,
) {
stop_rustdesk_servers();
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
match run_as_user(vec!["--server"], Some((cur_uid, cur_user))) {
Ok(ps) => user_server = ps,
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
force_stop_server();
start_server(Some((cur_uid, cur_user)), &mut user_server);
}
}
} else {
stop_rustdesk_servers();
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
force_stop_server();
stop_server(&mut user_server);
stop_server(&mut server);
}
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL));
}
if let Some(ps) = user_server.take().as_mut() {
@ -361,7 +382,7 @@ pub fn get_active_userid() -> String {
}
fn get_cm() -> bool {
if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() {
if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() {
if line.contains(&format!(
"{} --cm",
@ -379,7 +400,7 @@ fn get_cm() -> bool {
fn get_display() -> String {
let user = get_active_username();
log::debug!("w {}", &user);
if let Ok(output) = std::process::Command::new("w").arg(&user).output() {
if let Ok(output) = Command::new("w").arg(&user).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() {
log::debug!(" {}", line);
let mut iter = line.split_whitespace();
@ -394,7 +415,7 @@ fn get_display() -> String {
// above not work for gdm user
log::debug!("ls -l /tmp/.X11-unix/");
let mut last = "".to_owned();
if let Ok(output) = std::process::Command::new("ls")
if let Ok(output) = Command::new("ls")
.args(vec!["-l", "/tmp/.X11-unix/"])
.output()
{
@ -473,10 +494,7 @@ fn is_opensuse() -> bool {
false
}
pub fn run_as_user(
arg: Vec<&str>,
user: Option<(String, String)>,
) -> ResultType<Option<std::process::Child>> {
pub fn run_as_user(arg: Vec<&str>, user: Option<(String, String)>) -> ResultType<Option<Child>> {
let (uid, username) = match user {
Some(id_name) => id_name,
None => get_active_user_id_name(),
@ -490,7 +508,7 @@ pub fn run_as_user(
args.insert(0, "-E");
}
let task = std::process::Command::new("sudo").args(args).spawn()?;
let task = Command::new("sudo").args(args).spawn()?;
Ok(Some(task))
}
@ -552,10 +570,7 @@ pub fn get_default_pa_source() -> Option<(String, String)> {
}
pub fn lock_screen() {
std::process::Command::new("xdg-screensaver")
.arg("lock")
.spawn()
.ok();
Command::new("xdg-screensaver").arg("lock").spawn().ok();
}
pub fn toggle_blank_screen(_v: bool) {
@ -576,7 +591,7 @@ fn get_env_tries(name: &str, uid: &str, n: usize) -> String {
if !x.is_empty() {
return x;
}
std::thread::sleep(std::time::Duration::from_millis(300));
std::thread::sleep(Duration::from_millis(300));
}
"".to_owned()
}
@ -603,12 +618,12 @@ pub fn quit_gui() {
pub fn check_super_user_permission() -> ResultType<bool> {
let file = "/usr/share/rustdesk/files/polkit";
let arg;
if std::path::Path::new(file).is_file() {
if Path::new(file).is_file() {
arg = file;
} else {
arg = "echo";
}
let status = std::process::Command::new("pkexec").arg(arg).status()?;
let status = Command::new("pkexec").arg(arg).status()?;
Ok(status.success() && status.code() == Some(0))
}
@ -641,3 +656,55 @@ pub fn get_double_click_time() -> u32 {
double_click_time
}
}
pub fn resolutions(name: &str) -> Vec<Resolution> {
let mut v = vec![];
let mut parser = Parser::new();
if parser.parse().is_ok() {
if let Ok(connector) = parser.get_connector(name) {
if let Ok(resolutions) = &connector.available_resolutions() {
for r in resolutions {
if let Ok(width) = r.horizontal.parse::<i32>() {
if let Ok(height) = r.vertical.parse::<i32>() {
let resolution = Resolution {
width,
height,
..Default::default()
};
if !v.contains(&resolution) {
v.push(resolution);
}
}
}
}
}
}
}
v
}
pub fn current_resolution(name: &str) -> ResultType<Resolution> {
let mut parser = Parser::new();
parser.parse().map_err(|e| anyhow!(e))?;
let connector = parser.get_connector(name).map_err(|e| anyhow!(e))?;
let r = connector.current_resolution();
let width = r.horizontal.parse::<i32>()?;
let height = r.vertical.parse::<i32>()?;
Ok(Resolution {
width,
height,
..Default::default()
})
}
pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> {
Command::new("xrandr")
.args(vec![
"--output",
name,
"--mode",
&format!("{}x{}", width, height),
])
.spawn()?;
Ok(())
}

View File

@ -40,3 +40,113 @@ extern "C" float BackingScaleFactor() {
if (s) return [s backingScaleFactor];
return 1;
}
// https://github.com/jhford/screenresolution/blob/master/cg_utils.c
// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m
extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) {
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
if (allModes == NULL) {
return false;
}
*numModes = CFArrayGetCount(allModes);
CFRelease(allModes);
return true;
}
extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) {
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
if (allModes == NULL) {
return false;
}
*numModes = CFArrayGetCount(allModes);
for (uint32_t i = 0; i < *numModes && i < max; i++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
widths[i] = (uint32_t)CGDisplayModeGetWidth(mode);
heights[i] = (uint32_t)CGDisplayModeGetHeight(mode);
}
CFRelease(allModes);
return true;
}
extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t *height) {
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display);
if (mode == NULL) {
return false;
}
*width = (uint32_t)CGDisplayModeGetWidth(mode);
*height = (uint32_t)CGDisplayModeGetHeight(mode);
CGDisplayModeRelease(mode);
return true;
}
size_t bitDepth(CGDisplayModeRef mode) {
size_t depth = 0;
CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode);
// my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels
// are made up and possibly non-sensical
if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) {
depth = 96;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 64;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) {
depth = 48;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 32;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 30;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) {
depth = 16;
} else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) {
depth = 8;
}
CFRelease(pixelEncoding);
return depth;
}
bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) {
CGError rc;
CGDisplayConfigRef config;
rc = CGBeginDisplayConfiguration(&config);
if (rc != kCGErrorSuccess) {
return false;
}
rc = CGConfigureDisplayWithDisplayMode(config, display, mode, NULL);
if (rc != kCGErrorSuccess) {
return false;
}
rc = CGCompleteDisplayConfiguration(config, kCGConfigureForSession);
if (rc != kCGErrorSuccess) {
return false;
}
return true;
}
extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height)
{
bool ret = false;
CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display);
if (currentMode == NULL) {
return ret;
}
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL);
if (allModes == NULL) {
CGDisplayModeRelease(currentMode);
return ret;
}
int numModes = CFArrayGetCount(allModes);
for (int i = 0; i < numModes; i++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
if (width == CGDisplayModeGetWidth(mode) &&
height == CGDisplayModeGetHeight(mode) &&
bitDepth(currentMode) == bitDepth(mode) &&
CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) {
ret = setDisplayToMode(display, mode);
break;
}
}
CGDisplayModeRelease(currentMode);
CFRelease(allModes);
return ret;
}

View File

@ -17,7 +17,7 @@ use core_graphics::{
display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo},
window::{kCGWindowName, kCGWindowOwnerPID},
};
use hbb_common::{allow_err, bail, log};
use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution};
use include_dir::{include_dir, Dir};
use objc::{class, msg_send, sel, sel_impl};
use scrap::{libc::c_void, quartz::ffi::*};
@ -34,6 +34,16 @@ extern "C" {
static kAXTrustedCheckOptionPrompt: CFStringRef;
fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL;
fn InputMonitoringAuthStatus(_: BOOL) -> BOOL;
fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL;
fn MacGetModes(
display: u32,
widths: *mut u32,
heights: *mut u32,
max: u32,
numModes: *mut u32,
) -> BOOL;
fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL;
fn MacSetMode(display: u32, width: u32, height: u32) -> BOOL;
}
pub fn is_process_trusted(prompt: bool) -> bool {
@ -594,3 +604,64 @@ pub fn handle_application_should_open_untitled_file() {
}
}
}
pub fn resolutions(name: &str) -> Vec<Resolution> {
let mut v = vec![];
if let Ok(display) = name.parse::<u32>() {
let mut num = 0;
unsafe {
if YES == MacGetModeNum(display, &mut num) {
let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]);
let mut real_num = 0;
if YES
== MacGetModes(
display,
widths.as_mut_ptr(),
heights.as_mut_ptr(),
num,
&mut real_num,
)
{
if real_num <= num {
for i in 0..real_num {
let resolution = Resolution {
width: widths[i as usize] as _,
height: heights[i as usize] as _,
..Default::default()
};
if !v.contains(&resolution) {
v.push(resolution);
}
}
}
}
}
}
}
v
}
pub fn current_resolution(name: &str) -> ResultType<Resolution> {
let display = name.parse::<u32>().map_err(|e| anyhow!(e))?;
unsafe {
let (mut width, mut height) = (0, 0);
if NO == MacGetMode(display, &mut width, &mut height) {
bail!("MacGetMode failed");
}
Ok(Resolution {
width: width as _,
height: height as _,
..Default::default()
})
}
}
pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> {
let display = name.parse::<u32>().map_err(|e| anyhow!(e))?;
unsafe {
if NO == MacSetMode(display, width as _, height as _) {
bail!("MacSetMode failed");
}
}
Ok(())
}

View File

@ -74,5 +74,13 @@ mod tests {
assert!(!get_cursor_pos().is_none());
}
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[test]
fn test_resolution() {
let name = r"\\.\DISPLAY1";
println!("current:{:?}", current_resolution(name));
println!("change:{:?}", change_resolution(name, 2880, 1800));
println!("resolutions:{:?}", resolutions(name));
}
}

View File

@ -5,7 +5,9 @@ use crate::license::*;
use hbb_common::{
allow_err, bail,
config::{self, Config},
log, sleep, timeout, tokio,
log,
message_proto::Resolution,
sleep, timeout, tokio,
};
use std::io::prelude::*;
use std::{
@ -1784,3 +1786,89 @@ pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> {
.spawn()?;
Ok(())
}
pub fn resolutions(name: &str) -> Vec<Resolution> {
unsafe {
let mut dm: DEVMODEW = std::mem::zeroed();
let wname = wide_string(name);
let len = if wname.len() <= dm.dmDeviceName.len() {
wname.len()
} else {
dm.dmDeviceName.len()
};
std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len);
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
let mut v = vec![];
let mut num = 0;
loop {
if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 {
break;
}
let r = Resolution {
width: dm.dmPelsWidth as _,
height: dm.dmPelsHeight as _,
..Default::default()
};
if !v.contains(&r) {
v.push(r);
}
num += 1;
}
v
}
}
pub fn current_resolution(name: &str) -> ResultType<Resolution> {
unsafe {
let mut dm: DEVMODEW = std::mem::zeroed();
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
let wname = wide_string(name);
if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 {
bail!(
"failed to get currrent resolution, errno={}",
GetLastError()
);
}
let r = Resolution {
width: dm.dmPelsWidth as _,
height: dm.dmPelsHeight as _,
..Default::default()
};
Ok(r)
}
}
pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> {
unsafe {
let mut dm: DEVMODEW = std::mem::zeroed();
if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) {
bail!("EnumDisplaySettingsW failed, errno={}", GetLastError());
}
let wname = wide_string(name);
let len = if wname.len() <= dm.dmDeviceName.len() {
wname.len()
} else {
dm.dmDeviceName.len()
};
std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len);
dm.dmSize = std::mem::size_of::<DEVMODEW>() as _;
dm.dmPelsWidth = width as _;
dm.dmPelsHeight = height as _;
dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH;
let res = ChangeDisplaySettingsExW(
wname.as_ptr(),
&mut dm,
NULL as _,
CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET,
NULL,
);
if res != DISP_CHANGE_SUCCESSFUL {
bail!(
"ChangeDisplaySettingsExW failed, res={}, errno={}",
res,
GetLastError()
);
}
Ok(())
}
}

View File

@ -123,8 +123,10 @@ pub struct Connection {
#[cfg(windows)]
portable: PortableState,
from_switch: bool,
origin_resolution: HashMap<String, Resolution>,
voice_call_request_timestamp: Option<NonZeroI64>,
audio_input_device_before_voice_call: Option<String>,
options_in_login: Option<OptionMessage>,
}
impl ConnInner {
@ -228,9 +230,11 @@ impl Connection {
#[cfg(windows)]
portable: Default::default(),
from_switch: false,
origin_resolution: Default::default(),
audio_sender: None,
voice_call_request_timestamp: None,
audio_input_device_before_voice_call: None,
options_in_login: None,
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
tokio::spawn(async move {
@ -533,6 +537,8 @@ impl Connection {
conn.post_conn_audit(json!({
"action": "close",
}));
#[cfg(not(any(target_os = "android", target_os = "ios")))]
conn.reset_resolution();
ALIVE_CONNS.lock().unwrap().retain(|&c| c != id);
if let Some(s) = conn.server.upgrade() {
s.write().unwrap().remove_connection(&conn.inner);
@ -881,6 +887,16 @@ impl Connection {
..Default::default()
})
.into();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
pi.resolutions = Some(SupportedResolutions {
resolutions: video_service::get_current_display_name()
.map(|name| crate::platform::resolutions(&name))
.unwrap_or(vec![]),
..Default::default()
})
.into();
}
let mut sub_service = false;
if self.file_transfer.is_some() {
@ -907,6 +923,9 @@ impl Connection {
let mut msg_out = Message::new();
msg_out.set_login_response(res);
self.send(msg_out).await;
if let Some(o) = self.options_in_login.take() {
self.update_options(&o).await;
}
if let Some((dir, show_hidden)) = self.file_transfer.clone() {
let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() {
&dir
@ -1092,8 +1111,7 @@ impl Connection {
async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) {
self.lr = lr.clone();
if let Some(o) = lr.option.as_ref() {
// It may not be a good practice to update all options here.
self.update_options(o).await;
self.options_in_login = Some(o.clone());
if let Some(q) = o.video_codec_state.clone().take() {
scrap::codec::Encoder::update_video_encoder(
self.inner.id(),
@ -1538,7 +1556,6 @@ impl Connection {
.err()
.map_or("".to_string(), |e| e.to_string());
}
self.portable.elevation_requested = err.is_empty();
let mut misc = Misc::new();
misc.set_elevation_response(err);
let mut msg = Message::new();
@ -1557,7 +1574,6 @@ impl Connection {
.err()
.map_or("".to_string(), |e| e.to_string());
}
self.portable.elevation_requested = err.is_empty();
let mut misc = Misc::new();
misc.set_elevation_response(err);
let mut msg = Message::new();
@ -1597,6 +1613,26 @@ impl Connection {
return false;
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::ChangeResolution(r)) => {
if self.keyboard {
if let Ok(name) = video_service::get_current_display_name() {
if let Ok(current) = crate::platform::current_resolution(&name) {
if let Err(e) = crate::platform::change_resolution(
&name,
r.width as _,
r.height as _,
) {
log::error!("change resolution failed:{:?}", e);
} else {
if !self.origin_resolution.contains_key(&name) {
self.origin_resolution.insert(name, current);
}
}
}
}
}
}
_ => {}
},
Some(message::Union::AudioFrame(frame)) => {
@ -1665,7 +1701,8 @@ impl Connection {
self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
}
async fn update_options_without_auth(&mut self, o: &OptionMessage) {
async fn update_options(&mut self, o: &OptionMessage) {
log::info!("Option update: {:?}", o);
if let Ok(q) = o.image_quality.enum_value() {
let image_quality;
if let ImageQuality::NotSet = q {
@ -1696,12 +1733,6 @@ impl Connection {
scrap::codec::EncoderUpdate::State(q),
);
}
}
async fn update_options_with_auth(&mut self, o: &OptionMessage) {
if !self.authorized {
return;
}
if let Ok(q) = o.lock_after_session_end.enum_value() {
if q != BoolOption::NotSet {
self.lock_after_session_end = q == BoolOption::Yes;
@ -1830,12 +1861,6 @@ impl Connection {
}
}
async fn update_options(&mut self, o: &OptionMessage) {
log::info!("Option update: {:?}", o);
self.update_options_without_auth(o).await;
self.update_options_with_auth(o).await;
}
async fn on_close(&mut self, reason: &str, lock: bool) {
log::info!("#{} Connection closed: {}", self.inner.id(), reason);
if lock && self.lock_after_session_end && self.keyboard {
@ -1902,13 +1927,11 @@ impl Connection {
let p = &mut self.portable;
if running != p.last_running {
p.last_running = running;
if running && p.elevation_requested {
let mut misc = Misc::new();
misc.set_portable_service_running(running);
let mut msg = Message::new();
msg.set_misc(misc);
self.inner.send(msg.into());
}
let mut misc = Misc::new();
misc.set_portable_service_running(running);
let mut msg = Message::new();
msg.set_misc(misc);
self.inner.send(msg.into());
}
let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone();
if p.last_uac != uac {
@ -1937,6 +1960,20 @@ impl Connection {
}
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn reset_resolution(&self) {
self.origin_resolution
.iter()
.map(|(name, r)| {
if let Err(e) =
crate::platform::change_resolution(&name, r.width as _, r.height as _)
{
log::error!("change resolution failed:{:?}", e);
}
})
.count();
}
}
pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
@ -2118,7 +2155,6 @@ pub struct PortableState {
pub last_foreground_window_elevated: bool,
pub last_running: bool,
pub is_installed: bool,
pub elevation_requested: bool,
}
#[cfg(windows)]
@ -2129,7 +2165,6 @@ impl Default for PortableState {
last_uac: Default::default(),
last_foreground_window_elevated: Default::default(),
last_running: Default::default(),
elevation_requested: Default::default(),
}
}
}

View File

@ -356,7 +356,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
let (ndisplay, current, display) = get_current_display()?;
let (origin, width, height) = (display.origin(), display.width(), display.height());
log::debug!(
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}",
"#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}",
ndisplay,
current,
&origin,
@ -364,6 +364,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
height,
num_cpus::get_physical(),
num_cpus::get(),
display.name(),
);
let privacy_mode_id = *PRIVACY_MODE_CONN_ID.lock().unwrap();
@ -501,6 +502,14 @@ fn run(sp: GenericService) -> ResultType<()> {
width: c.width as _,
height: c.height as _,
cursor_embedded: capture_cursor_embedded(),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
resolutions: Some(SupportedResolutions {
resolutions: get_current_display_name()
.map(|name| crate::platform::resolutions(&name))
.unwrap_or(vec![]),
..SupportedResolutions::default()
})
.into(),
..Default::default()
});
let mut msg_out = Message::new();
@ -568,6 +577,14 @@ fn run(sp: GenericService) -> ResultType<()> {
if last_check_displays.elapsed().as_millis() > 1000 {
last_check_displays = now;
// Capturer on macos does not return Err event the solution is changed.
#[cfg(target_os = "macos")]
if check_display_changed(c.ndisplay, c.current, c.width, c.height) {
log::info!("Displays changed");
*SWITCH.lock().unwrap() = true;
bail!("SWITCH");
}
if let Some(msg_out) = check_displays_changed() {
sp.send(msg_out);
}
@ -992,6 +1009,10 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> {
get_current_display_2(try_get_displays()?)
}
pub fn get_current_display_name() -> ResultType<String> {
Ok(get_current_display_2(try_get_displays()?)?.2.name())
}
#[cfg(windows)]
fn start_uac_elevation_check() {
static START: Once = Once::new();

View File

@ -9,7 +9,7 @@ use sciter::Value;
use hbb_common::{
allow_err,
config::{self, LocalConfig, PeerConfig},
config::{LocalConfig, PeerConfig},
log,
};
@ -413,17 +413,15 @@ impl UI {
}
fn remove_discovered(&mut self, id: String) {
let mut peers = config::LanPeers::load().peers;
peers.retain(|x| x.id != id);
config::LanPeers::store(&peers);
remove_discovered(id);
}
fn send_wol(&mut self, id: String) {
crate::lan::send_wol(id)
}
fn new_remote(&mut self, id: String, remote_type: String) {
new_remote(id, remote_type)
fn new_remote(&mut self, id: String, remote_type: String, force_relay: bool) {
new_remote(id, remote_type, force_relay)
}
fn is_process_trusted(&mut self, _prompt: bool) -> bool {
@ -573,6 +571,10 @@ impl UI {
fn default_video_save_directory(&self) -> String {
default_video_save_directory()
}
fn handle_relay_id(&self, id: String) -> String {
handle_relay_id(id)
}
}
impl sciter::EventHandler for UI {
@ -590,7 +592,7 @@ impl sciter::EventHandler for UI {
fn set_remote_id(String);
fn closing(i32, i32, i32, i32);
fn get_size();
fn new_remote(String, bool);
fn new_remote(String, String, bool);
fn send_wol(String);
fn remove_peer(String);
fn remove_discovered(String);
@ -655,6 +657,7 @@ impl sciter::EventHandler for UI {
fn has_hwcodec();
fn get_langs();
fn default_video_save_directory();
fn handle_relay_id(String);
}
}
@ -720,9 +723,13 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc<Vec<Value>> {
}
#[inline]
pub fn new_remote(id: String, remote_type: String) {
pub fn new_remote(id: String, remote_type: String, force_relay: bool) {
let mut lock = CHILDREN.lock().unwrap();
let args = vec![format!("--{}", remote_type), id.clone()];
let mut args = vec![format!("--{}", remote_type), id.clone()];
if force_relay {
args.push("".to_string()); // password
args.push("--relay".to_string());
}
let key = (id.clone(), remote_type.clone());
if let Some(c) = lock.1.get_mut(&key) {
if let Ok(Some(_)) = c.try_wait() {

View File

@ -62,12 +62,15 @@ function createNewConnect(id, type) {
id = id.replace(/\s/g, "");
app.remote_id.value = formatId(id);
if (!id) return;
var old_id = id;
id = handler.handle_relay_id(id);
var force_relay = old_id != id;
if (id == my_id) {
msgbox("custom-error", "Error", "You cannot connect to your own computer");
return;
}
handler.set_remote_id(id);
handler.new_remote(id, type);
handler.new_remote(id, type, force_relay);
}
class ShareRdp: Reactor.Component {

View File

@ -277,6 +277,8 @@ impl InvokeUiSession for SciterHandler {
fn switch_back(&self, _id: &str) {}
fn portable_service_running(&self, _running: bool) {}
fn on_voice_call_started(&self) {
self.call("onVoiceCallStart", &make_args!());
}
@ -460,6 +462,7 @@ impl sciter::EventHandler for SciterSession {
impl SciterSession {
pub fn new(cmd: String, id: String, password: String, args: Vec<String>) -> Self {
let force_relay = args.contains(&"--relay".to_string());
let session: Session<SciterHandler> = Session {
id: id.clone(),
password: password.clone(),
@ -484,7 +487,7 @@ impl SciterSession {
.lc
.write()
.unwrap()
.initialize(id, conn_type, None, false);
.initialize(id, conn_type, None, force_relay);
Self(session)
}

View File

@ -596,6 +596,13 @@ pub fn get_lan_peers() -> Vec<HashMap<&'static str, String>> {
.collect()
}
#[inline]
pub fn remove_discovered(id: String) {
let mut peers = config::LanPeers::load().peers;
peers.retain(|x| x.id != id);
config::LanPeers::store(&peers);
}
#[inline]
pub fn get_uuid() -> String {
base64::encode(hbb_common::get_uuid())
@ -963,3 +970,12 @@ async fn check_id(
}
""
}
// if it's relay id, return id processed, otherwise return original id
pub fn handle_relay_id(id: String) -> String {
if id.ends_with(r"\r") || id.ends_with(r"/r") {
id[0..id.len() - 2].to_string()
} else {
id
}
}

View File

@ -713,6 +713,18 @@ impl<T: InvokeUiSession> Session<T> {
}
}
pub fn change_resolution(&self, width: i32, height: i32) {
let mut misc = Misc::new();
misc.set_change_resolution(Resolution {
width,
height,
..Default::default()
});
let mut msg = Message::new();
msg.set_misc(misc);
self.send(Data::Message(msg));
}
pub fn request_voice_call(&self) {
self.send(Data::NewVoiceCall);
}
@ -793,6 +805,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
fn clipboard(&self, content: String);
fn cancel_msgbox(&self, tag: &str);
fn switch_back(&self, id: &str);
fn portable_service_running(&self, running: bool);
fn on_voice_call_started(&self);
fn on_voice_call_closed(&self, reason: &str);
fn on_voice_call_waiting(&self);

1
vdi/README.md Normal file
View File

@ -0,0 +1 @@
# WIP

View File

@ -0,0 +1,16 @@
FROM rockylinux:9.1
ENV HOME=/home/vscode
ENV WORKDIR=$HOME/rustdesk/vdi/host
# https://ciq.co/blog/top-10-things-to-do-after-rocky-linux-9-install/ also gpu driver install
WORKDIR $HOME
RUN dnf -y install epel-release
RUN dnf config-manager --set-enabled crb
RUN dnf -y install cargo libvpx-devel opus-devel usbredir-devel git cmake gcc-c++ pkg-config nasm yasm ninja-build automake libtool libva-devel libvdpau-devel llvm-devel
WORKDIR /
RUN git clone https://chromium.googlesource.com/libyuv/libyuv
WORKDIR /libyuv
RUN cmake . -DCMAKE_INSTALL_PREFIX=/user
RUN make -j4 && make install
WORKDIR /

View File

@ -0,0 +1,27 @@
{
"name": "rustdesk",
"build": {
"dockerfile": "./Dockerfile",
"context": "."
},
"workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache",
"workspaceFolder": "/home/vscode/rustdesk",
"customizations": {
"vscode": {
"extensions": [
"vadimcn.vscode-lldb",
"mutantdino.resourcemonitor",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"serayuzgur.crates",
"mhutchie.git-graph",
"eamodio.gitlens"
],
"settings": {
"files.watcherExclude": {
"**/target/**": true
}
}
}
}
}

1
vdi/host/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1185
vdi/host/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
vdi/host/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "qemu-rustdesk"
version = "0.1.0"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
[dependencies]
qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" }

1
vdi/host/README.md Normal file
View File

@ -0,0 +1 @@
# RustDesk protocol on QEMU D-Bus display

2
vdi/host/src/main.rs Normal file
View File

@ -0,0 +1,2 @@
fn main() {
}