Merge branch 'master'
This commit is contained in:
commit
0a3b5f10a6
72
Cargo.lock
generated
72
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
53
flutter/lib/desktop/widgets/dragable_divider.dart
Normal file
53
flutter/lib/desktop/widgets/dragable_divider.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
@ -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(
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
25
src/ui.rs
25
src/ui.rs
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
1
vdi/README.md
Normal file
@ -0,0 +1 @@
|
||||
# WIP
|
16
vdi/host/.devcontainer/Dockerfile
Normal file
16
vdi/host/.devcontainer/Dockerfile
Normal 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 /
|
27
vdi/host/.devcontainer/devcontainer.json
Normal file
27
vdi/host/.devcontainer/devcontainer.json
Normal 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
1
vdi/host/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1185
vdi/host/Cargo.lock
generated
Normal file
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
9
vdi/host/Cargo.toml
Normal 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
1
vdi/host/README.md
Normal file
@ -0,0 +1 @@
|
||||
# RustDesk protocol on QEMU D-Bus display
|
2
vdi/host/src/main.rs
Normal file
2
vdi/host/src/main.rs
Normal file
@ -0,0 +1,2 @@
|
||||
fn main() {
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user