mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'rustdesk:master' into master
This commit is contained in:
commit
6915b88d8c
@ -10,7 +10,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/java:1": {},
|
||||
"ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": {
|
||||
"PACKAGES": "platform-tools,ndk;22.1.7171670"
|
||||
"PACKAGES": "platform-tools,ndk;23.2.8568313"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
@ -31,4 +31,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-18.04,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
|
||||
1602
.github/workflows/flutter-build.yml
vendored
Normal file
1602
.github/workflows/flutter-build.yml
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
.github/workflows/flutter-ci.yml
vendored
2
.github/workflows/flutter-ci.yml
vendored
@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
run-ci:
|
||||
uses: ./.github/workflows/flutter-nightly.yml
|
||||
uses: ./.github/workflows/flutter-build.yml
|
||||
with:
|
||||
upload-artifact: false
|
||||
|
||||
1617
.github/workflows/flutter-nightly.yml
vendored
1617
.github/workflows/flutter-nightly.yml
vendored
File diff suppressed because it is too large
Load Diff
56
Cargo.lock
generated
56
Cargo.lock
generated
@ -4159,6 +4159,38 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pam"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/fufesou/pam#10da2cbbabe32cbc9de22a66abe44738e7ec0ea0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pam-macros",
|
||||
"pam-sys",
|
||||
"users 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam-macros"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.54",
|
||||
"quote 1.0.26",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam-sys"
|
||||
version = "1.0.0-alpha4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e9dfd42858f6a6bb1081079fd9dc259ca3e2aaece6cb689fd36b1058046c969"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.16.5"
|
||||
@ -5124,6 +5156,7 @@ dependencies = [
|
||||
"objc",
|
||||
"objc_id",
|
||||
"os-version",
|
||||
"pam",
|
||||
"parity-tokio-ipc",
|
||||
"rdev",
|
||||
"repng",
|
||||
@ -5149,6 +5182,7 @@ dependencies = [
|
||||
"tray-icon",
|
||||
"trayicon",
|
||||
"url",
|
||||
"users 0.11.0",
|
||||
"uuid",
|
||||
"virtual_display",
|
||||
"whoami",
|
||||
@ -6343,7 +6377,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "trayicon"
|
||||
version = "0.1.3-1"
|
||||
source = "git+https://github.com/open-trade/trayicon-rs#8d9c4489287752cc5be4a35c103198f7111112f9"
|
||||
source = "git+https://github.com/open-trade/trayicon-rs#35bd01963271b45a0b6a0f65f1ce03a5f35bc691"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
"winit",
|
||||
@ -6443,6 +6477,26 @@ dependencies = [
|
||||
"serde 1.0.159",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
|
||||
@ -30,6 +30,7 @@ flutter = ["flutter_rust_bridge"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
mediacodec = ["scrap/mediacodec"]
|
||||
linux_headless = ["pam", "users"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -122,6 +123,8 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
|
||||
evdev = { git="https://github.com/fufesou/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
||||
users = { version = "0.11.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.11"
|
||||
|
||||
19
build.py
19
build.py
@ -287,6 +287,8 @@ def build_flutter_deb(version, features):
|
||||
system2('flutter build linux --release')
|
||||
system2('mkdir -p tmpdeb/usr/bin/')
|
||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||
system2('mkdir -p tmpdeb/etc/rustdesk/')
|
||||
system2('mkdir -p tmpdeb/etc/pam.d/')
|
||||
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
|
||||
system2('mkdir -p tmpdeb/usr/share/applications/')
|
||||
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
|
||||
@ -303,6 +305,12 @@ def build_flutter_deb(version, features):
|
||||
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2(
|
||||
'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/')
|
||||
system2(
|
||||
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||
system2(
|
||||
'cp ../res/xorg.conf tmpdeb/etc/rustdesk/')
|
||||
system2(
|
||||
'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||
system2(
|
||||
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
|
||||
|
||||
@ -553,12 +561,21 @@ def main():
|
||||
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
|
||||
system2(
|
||||
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
|
||||
system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||
os.system('mkdir -p tmpdeb/etc/rustdesk/')
|
||||
os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/')
|
||||
os.system('mkdir -p tmpdeb/etc/X11/rustdesk/')
|
||||
os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/')
|
||||
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
|
||||
os.system('mkdir -p tmpdeb/etc/pam.d/')
|
||||
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
|
||||
system2('strip tmpdeb/usr/bin/rustdesk')
|
||||
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
|
||||
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
|
||||
system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
|
||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||
md5_file('etc/rustdesk/startwm.sh')
|
||||
md5_file('etc/X11/rustdesk/xorg.conf')
|
||||
md5_file('etc/pam.d/rustdesk')
|
||||
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
|
||||
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
||||
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
||||
|
||||
@ -58,13 +58,12 @@ function build {
|
||||
fi
|
||||
make clean
|
||||
./configure --target=$LIBVPX_TARGET \
|
||||
--enable-pic --disable-vp8 \
|
||||
--enable-pic
|
||||
--disable-webm-io \
|
||||
--disable-unit-tests \
|
||||
--disable-examples \
|
||||
--disable-libyuv \
|
||||
--disable-postproc \
|
||||
--disable-vp8 \
|
||||
--disable-tools \
|
||||
--disable-docs \
|
||||
--prefix=$PREFIX
|
||||
@ -122,4 +121,4 @@ build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
|
||||
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
|
||||
|
||||
# rm -rf build/libvpx
|
||||
# rm -rf build/oboe
|
||||
# rm -rf build/oboe
|
||||
|
||||
@ -456,7 +456,7 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
await _connectDialog(
|
||||
id,
|
||||
dialogManager,
|
||||
peerPasswordController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -464,8 +464,8 @@ void enterUserLoginDialog(String id, OverlayDialogManager dialogManager) async {
|
||||
await _connectDialog(
|
||||
id,
|
||||
dialogManager,
|
||||
usernameController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -474,20 +474,27 @@ void enterUserLoginAndPasswordDialog(
|
||||
await _connectDialog(
|
||||
id,
|
||||
dialogManager,
|
||||
usernameController: TextEditingController(),
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
peerPasswordController: TextEditingController(),
|
||||
);
|
||||
}
|
||||
|
||||
_connectDialog(
|
||||
String id,
|
||||
OverlayDialogManager dialogManager, {
|
||||
TextEditingController? usernameController,
|
||||
TextEditingController? osUsernameController,
|
||||
TextEditingController? osPasswordController,
|
||||
TextEditingController? passwordController,
|
||||
TextEditingController? peerPasswordController,
|
||||
}) async {
|
||||
var remember = await bind.sessionGetRemember(id: id) ?? false;
|
||||
var rememberPassword = false;
|
||||
if (passwordController != null) {
|
||||
rememberPassword = await bind.sessionGetRemember(id: id) ?? false;
|
||||
}
|
||||
var rememberAccount = false;
|
||||
if (osUsernameController != null) {
|
||||
rememberAccount = await bind.sessionGetRemember(id: id) ?? false;
|
||||
}
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
@ -496,82 +503,129 @@ _connectDialog(
|
||||
}
|
||||
|
||||
submit() {
|
||||
// to-do:
|
||||
// username and password are about remote OS account.
|
||||
// If the remote side is headless.
|
||||
// The client side should login to remote OS account, to enable X desktop session.
|
||||
// `username` and `password` will be used in the near future.
|
||||
final username = usernameController?.text.trim() ?? '';
|
||||
final osUsername = osUsernameController?.text.trim() ?? '';
|
||||
final osPassword = osPasswordController?.text.trim() ?? '';
|
||||
final password = passwordController?.text.trim() ?? '';
|
||||
final peerPassword = peerPasswordController?.text.trim() ?? '';
|
||||
if (peerPasswordController != null && peerPassword.isEmpty) return;
|
||||
if (passwordController != null && password.isEmpty) return;
|
||||
if (rememberAccount) {
|
||||
bind.sessionPeerOption(id: id, name: 'os-username', value: osUsername);
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: osPassword);
|
||||
}
|
||||
gFFI.login(
|
||||
// username,
|
||||
// password,
|
||||
osUsername,
|
||||
osPassword,
|
||||
id,
|
||||
peerPassword,
|
||||
remember,
|
||||
password,
|
||||
rememberPassword,
|
||||
);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
|
||||
descWidget(String text) {
|
||||
return Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 3,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
rememberWidget(
|
||||
String desc,
|
||||
bool remember,
|
||||
ValueChanged<bool?>? onChanged,
|
||||
) {
|
||||
return CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(desc),
|
||||
value: remember,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
osAccountWidget() {
|
||||
if (osUsernameController == null || osPasswordController == null) {
|
||||
return Offstage();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
descWidget(translate('login_linux_tip')),
|
||||
DialogTextField(
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: osUsernameController,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: null,
|
||||
),
|
||||
PasswordWidget(
|
||||
controller: osPasswordController,
|
||||
autoFocus: false,
|
||||
),
|
||||
rememberWidget(
|
||||
translate('remember_account_tip'),
|
||||
rememberAccount,
|
||||
(v) {
|
||||
if (v != null) {
|
||||
setState(() => rememberAccount = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
passwdWidget() {
|
||||
if (passwordController == null) {
|
||||
return Offstage();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
descWidget(translate('verify_rustdesk_password_tip')),
|
||||
PasswordWidget(
|
||||
controller: passwordController,
|
||||
autoFocus: osUsernameController == null,
|
||||
),
|
||||
rememberWidget(
|
||||
translate('Remember password'),
|
||||
rememberPassword,
|
||||
(v) {
|
||||
if (v != null) {
|
||||
setState(() => rememberPassword = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
(usernameController == null
|
||||
? Text(translate('Password Required'))
|
||||
: Tooltip(
|
||||
message: translate('login_linux_tooltip_tip'),
|
||||
child: Text(translate('login_linux_tip')),
|
||||
))
|
||||
.paddingOnly(left: 10),
|
||||
Text(translate('Password Required')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
usernameController == null
|
||||
osAccountWidget(),
|
||||
osUsernameController == null || passwordController == null
|
||||
? Offstage()
|
||||
: DialogTextField(
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: usernameController,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: null,
|
||||
),
|
||||
passwordController == null
|
||||
? Offstage()
|
||||
: PasswordWidget(
|
||||
controller: passwordController,
|
||||
autoFocus: false,
|
||||
),
|
||||
usernameController == null || peerPasswordController == null
|
||||
? Offstage()
|
||||
: const Divider(),
|
||||
peerPasswordController == null
|
||||
? Offstage()
|
||||
: PasswordWidget(
|
||||
controller: peerPasswordController,
|
||||
autoFocus: usernameController == null,
|
||||
hintText: 'enter_rustdesk_passwd_tip',
|
||||
),
|
||||
peerPasswordController == null
|
||||
? Offstage()
|
||||
: CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('remember_rustdesk_passwd_tip'),
|
||||
),
|
||||
value: remember,
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
setState(() => remember = v);
|
||||
}
|
||||
},
|
||||
),
|
||||
: Container(height: 12),
|
||||
passwdWidget(),
|
||||
]),
|
||||
actions: [
|
||||
dialogButton(
|
||||
@ -847,3 +901,149 @@ void showRestartRemoteDevice(
|
||||
));
|
||||
if (res == true) bind.sessionRestartRemoteDevice(id: id);
|
||||
}
|
||||
|
||||
showSetOSPassword(
|
||||
String id,
|
||||
bool login,
|
||||
OverlayDialogManager dialogManager,
|
||||
) async {
|
||||
final controller = TextEditingController();
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != '';
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: 'auto-login', value: autoLogin ? 'Y' : '');
|
||||
if (text != '' && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('OS Password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
showSetOSAccount(
|
||||
String id,
|
||||
OverlayDialogManager dialogManager,
|
||||
) async {
|
||||
final usernameController = TextEditingController();
|
||||
final passwdController = TextEditingController();
|
||||
var username = await bind.sessionGetOption(id: id, arg: 'os-username') ?? '';
|
||||
var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
usernameController.text = username;
|
||||
passwdController.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
final username = usernameController.text.trim();
|
||||
final password = usernameController.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: 'os-username', value: username);
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: password);
|
||||
close();
|
||||
}
|
||||
|
||||
descWidget(String text) {
|
||||
return Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 3,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('OS Account')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
descWidget(translate("os_account_desk_tip")),
|
||||
DialogTextField(
|
||||
title: translate(DialogTextField.kUsernameTitle),
|
||||
controller: usernameController,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: null,
|
||||
),
|
||||
PasswordWidget(controller: passwdController),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -388,6 +388,15 @@ class BlockableOverlayState extends OverlayKeyState {
|
||||
_middleBlocked.value = blocked;
|
||||
}
|
||||
}
|
||||
|
||||
void applyFfi(FFI ffi) {
|
||||
ffi.dialogManager.setOverlayState(this);
|
||||
ffi.chatModel.setOverlayState(this);
|
||||
// make remote page penetrable automatically, effective for chat over remote
|
||||
onMiddleBlockedClick = () {
|
||||
setMiddleBlocked(false);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BlockableOverlay extends StatelessWidget {
|
||||
|
||||
@ -261,7 +261,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
},
|
||||
child: Icon(
|
||||
peerCardUiType.value == PeerUiType.grid
|
||||
? Icons.list_rounded
|
||||
? Icons.list
|
||||
: Icons.grid_view_rounded,
|
||||
size: 18,
|
||||
color: textColor,
|
||||
|
||||
@ -225,9 +225,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
children: [
|
||||
Button(
|
||||
isOutline: true,
|
||||
onTap: () {
|
||||
onConnect(isFileTransfer: true);
|
||||
},
|
||||
onTap: () => onConnect(isFileTransfer: true),
|
||||
text: "Transfer File",
|
||||
),
|
||||
const SizedBox(
|
||||
|
||||
@ -1228,9 +1228,9 @@ class _DisplayState extends State<_Display> {
|
||||
children: [
|
||||
Slider(
|
||||
value: fpsValue.value,
|
||||
min: 10.0,
|
||||
min: 5.0,
|
||||
max: 120.0,
|
||||
divisions: 22,
|
||||
divisions: 23,
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
await bind.mainSetUserDefaultOption(
|
||||
@ -1258,9 +1258,6 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
Widget codec(BuildContext context) {
|
||||
if (!bind.mainHasHwcodec()) {
|
||||
return Offstage();
|
||||
}
|
||||
final key = 'codec-preference';
|
||||
onChanged(String value) async {
|
||||
await bind.mainSetUserDefaultOption(key: key, value: value);
|
||||
@ -1268,28 +1265,45 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
||||
|
||||
var hwRadios = [];
|
||||
try {
|
||||
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
if (h264) {
|
||||
hwRadios.add(_Radio(context,
|
||||
value: 'h264',
|
||||
groupValue: groupValue,
|
||||
label: 'H264',
|
||||
onChanged: onChanged));
|
||||
}
|
||||
if (h265) {
|
||||
hwRadios.add(_Radio(context,
|
||||
value: 'h265',
|
||||
groupValue: groupValue,
|
||||
label: 'H265',
|
||||
onChanged: onChanged));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("failed to parse supported hwdecodings, err=$e");
|
||||
}
|
||||
return _Card(title: 'Default Codec', children: [
|
||||
_Radio(context,
|
||||
value: 'auto',
|
||||
groupValue: groupValue,
|
||||
label: 'Auto',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'vp8',
|
||||
groupValue: groupValue,
|
||||
label: 'VP8',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'vp9',
|
||||
groupValue: groupValue,
|
||||
label: 'VP9',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'h264',
|
||||
groupValue: groupValue,
|
||||
label: 'H264',
|
||||
onChanged: onChanged),
|
||||
_Radio(context,
|
||||
value: 'h265',
|
||||
groupValue: groupValue,
|
||||
label: 'H265',
|
||||
onChanged: onChanged),
|
||||
...hwRadios,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -158,12 +158,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
// _isCustomCursorInited = true;
|
||||
// }
|
||||
|
||||
_ffi.dialogManager.setOverlayState(_blockableOverlayState);
|
||||
_ffi.chatModel.setOverlayState(_blockableOverlayState);
|
||||
// make remote page penetrable automatically, effective for chat over remote
|
||||
_blockableOverlayState.onMiddleBlockedClick = () {
|
||||
_blockableOverlayState.setMiddleBlocked(false);
|
||||
};
|
||||
_blockableOverlayState.applyFfi(_ffi);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -320,6 +320,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
PeerInfo get pi => widget.ffi.ffiModel.pi;
|
||||
FfiModel get ffiModel => widget.ffi.ffiModel;
|
||||
|
||||
triggerAutoHide() => _debouncerHide.value = _debouncerHide.value + 1;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
@ -332,7 +334,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
|
||||
widget.onEnterOrLeaveImageSetter((enter) {
|
||||
if (enter) {
|
||||
_debouncerHide.value = 0;
|
||||
triggerAutoHide();
|
||||
_isCursorOverImage = true;
|
||||
} else {
|
||||
_isCursorOverImage = false;
|
||||
@ -367,7 +369,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
Widget _buildDraggableShowHide(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (show.isTrue && _dragging.isFalse) {
|
||||
_debouncerHide.value = 1;
|
||||
triggerAutoHide();
|
||||
}
|
||||
return Align(
|
||||
alignment: FractionalOffset(_fractionX.value, 0),
|
||||
@ -637,7 +639,7 @@ class _ControlMenu extends StatelessWidget {
|
||||
ffi: ffi,
|
||||
menuChildren: [
|
||||
requestElevation(),
|
||||
osPassword(),
|
||||
ffi.ffiModel.pi.is_headless ? osAccount() : osPassword(),
|
||||
transferFile(context),
|
||||
tcpTunneling(context),
|
||||
note(),
|
||||
@ -660,78 +662,20 @@ class _ControlMenu extends StatelessWidget {
|
||||
onPressed: () => showRequestElevationDialog(id, ffi.dialogManager));
|
||||
}
|
||||
|
||||
osAccount() {
|
||||
return _MenuItemButton(
|
||||
child: Text(translate('OS Account')),
|
||||
trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
|
||||
ffi: ffi,
|
||||
onPressed: () => showSetOSAccount(id, ffi.dialogManager));
|
||||
}
|
||||
|
||||
osPassword() {
|
||||
return _MenuItemButton(
|
||||
child: Text(translate('OS Password')),
|
||||
trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
|
||||
ffi: ffi,
|
||||
onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager));
|
||||
}
|
||||
|
||||
_showSetOSPassword(
|
||||
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
var password =
|
||||
await bind.sessionGetOption(id: id, arg: 'os-password') ?? '';
|
||||
var autoLogin =
|
||||
await bind.sessionGetOption(id: id, arg: 'auto-login') != '';
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: 'os-password', value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: 'auto-login', value: autoLogin ? 'Y' : '');
|
||||
if (text != '' && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.password_rounded, color: MyTheme.accent),
|
||||
Text(translate('OS Password')).paddingOnly(left: 10),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
onPressed: () => showSetOSPassword(id, false, ffi.dialogManager));
|
||||
}
|
||||
|
||||
transferFile(BuildContext context) {
|
||||
@ -1293,7 +1237,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
final fpsOption =
|
||||
await bind.sessionGetOption(id: widget.id, arg: 'custom-fps');
|
||||
fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30;
|
||||
if (fpsInitValue < 10 || fpsInitValue > 120) {
|
||||
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
||||
fpsInitValue = 30;
|
||||
}
|
||||
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
|
||||
@ -1316,9 +1260,9 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
children: [
|
||||
Obx((() => Slider(
|
||||
value: fpsSliderValue.value,
|
||||
min: 10,
|
||||
min: 5,
|
||||
max: 120,
|
||||
divisions: 22,
|
||||
divisions: 23,
|
||||
onChanged: (double value) {
|
||||
fpsSliderValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
@ -1349,29 +1293,30 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
|
||||
codec() {
|
||||
return futureBuilder(future: () async {
|
||||
final supportedHwcodec =
|
||||
await bind.sessionSupportedHwcodec(id: widget.id);
|
||||
final alternativeCodecs =
|
||||
await bind.sessionAlternativeCodecs(id: widget.id);
|
||||
final codecPreference =
|
||||
await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ??
|
||||
'';
|
||||
return {
|
||||
'supportedHwcodec': supportedHwcodec,
|
||||
'alternativeCodecs': alternativeCodecs,
|
||||
'codecPreference': codecPreference
|
||||
};
|
||||
}(), hasData: (data) {
|
||||
final List<bool> codecs = [];
|
||||
try {
|
||||
final Map codecsJson = jsonDecode(data['supportedHwcodec']);
|
||||
final Map codecsJson = jsonDecode(data['alternativeCodecs']);
|
||||
final vp8 = codecsJson['vp8'] ?? false;
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(vp8);
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
final visible = bind.mainHasHwcodec() &&
|
||||
codecs.length == 2 &&
|
||||
(codecs[0] || codecs[1]);
|
||||
final visible =
|
||||
codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2]);
|
||||
if (!visible) return Offstage();
|
||||
final groupValue = data['codecPreference'] as String;
|
||||
onChanged(String? value) async {
|
||||
@ -1392,6 +1337,13 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
onChanged: onChanged,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
_RadioMenuButton<String>(
|
||||
child: Text(translate('VP8')),
|
||||
value: 'vp8',
|
||||
groupValue: groupValue,
|
||||
onChanged: codecs[0] ? onChanged : null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
_RadioMenuButton<String>(
|
||||
child: Text(translate('VP9')),
|
||||
value: 'vp9',
|
||||
@ -1403,14 +1355,14 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
child: Text(translate('H264')),
|
||||
value: 'h264',
|
||||
groupValue: groupValue,
|
||||
onChanged: codecs[0] ? onChanged : null,
|
||||
onChanged: codecs[1] ? onChanged : null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
_RadioMenuButton<String>(
|
||||
child: Text(translate('H265')),
|
||||
value: 'h265',
|
||||
groupValue: groupValue,
|
||||
onChanged: codecs[1] ? onChanged : null,
|
||||
onChanged: codecs[2] ? onChanged : null,
|
||||
ffi: widget.ffi,
|
||||
),
|
||||
]);
|
||||
|
||||
@ -20,7 +20,6 @@ import '../../models/input_model.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../utils/image.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
import '../widgets/gestures.dart';
|
||||
|
||||
final initText = '\1' * 1024;
|
||||
@ -43,6 +42,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||
Orientation? _currentOrientation;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
final keyboardVisibilityController = KeyboardVisibilityController();
|
||||
late final StreamSubscription<bool> keyboardSubscription;
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
@ -67,6 +68,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
keyboardSubscription =
|
||||
keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
|
||||
_blockableOverlayState.applyFfi(gFFI);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -548,19 +550,39 @@ class _RemotePageState extends State<RemotePage> {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Refresh')), value: 'refresh'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: ([
|
||||
Text(translate('OS Password')),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
showSetOSPassword(id, false, gFFI.dialogManager);
|
||||
},
|
||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
||||
)
|
||||
])),
|
||||
value: 'enter_os_password'));
|
||||
if (gFFI.ffiModel.pi.is_headless) {
|
||||
more.add(
|
||||
PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: ([
|
||||
Text(translate('OS Account')),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
showSetOSAccount(id, gFFI.dialogManager);
|
||||
},
|
||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
||||
)
|
||||
])),
|
||||
value: 'enter_os_account'),
|
||||
);
|
||||
} else {
|
||||
more.add(
|
||||
PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: ([
|
||||
Text(translate('OS Password')),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
showSetOSPassword(id, false, gFFI.dialogManager);
|
||||
},
|
||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
||||
)
|
||||
])),
|
||||
value: 'enter_os_password'),
|
||||
);
|
||||
}
|
||||
if (!isWebDesktop) {
|
||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
@ -657,6 +679,8 @@ class _RemotePageState extends State<RemotePage> {
|
||||
} else {
|
||||
showSetOSPassword(id, true, gFFI.dialogManager);
|
||||
}
|
||||
} else if (value == 'enter_os_account') {
|
||||
showSetOSAccount(id, gFFI.dialogManager);
|
||||
} else if (value == 'reset_canvas') {
|
||||
gFFI.cursorModel.reset();
|
||||
} else if (value == 'restart') {
|
||||
@ -970,17 +994,17 @@ void showOptions(
|
||||
final perms = gFFI.ffiModel.permissions;
|
||||
final hasHwcodec = bind.mainHasHwcodec();
|
||||
final List<bool> codecs = [];
|
||||
if (hasHwcodec) {
|
||||
try {
|
||||
final Map codecsJson =
|
||||
jsonDecode(await bind.sessionSupportedHwcodec(id: id));
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
try {
|
||||
final Map codecsJson =
|
||||
jsonDecode(await bind.sessionAlternativeCodecs(id: id));
|
||||
final vp8 = codecsJson['vp8'] ?? false;
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
codecs.add(vp8);
|
||||
codecs.add(h264);
|
||||
codecs.add(h265);
|
||||
} catch (e) {
|
||||
debugPrint("Show Codec Preference err=$e");
|
||||
}
|
||||
|
||||
dialogManager.show((setState, close) {
|
||||
@ -1041,15 +1065,16 @@ void showOptions(
|
||||
const Divider(color: MyTheme.border)
|
||||
];
|
||||
|
||||
if (hasHwcodec && codecs.length == 2 && (codecs[0] || codecs[1])) {
|
||||
radios.addAll([
|
||||
getRadio(translate('Auto'), 'auto', codec, setCodec),
|
||||
getRadio('VP9', 'vp9', codec, setCodec),
|
||||
]);
|
||||
if (codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2])) {
|
||||
radios.add(getRadio(translate('Auto'), 'auto', codec, setCodec));
|
||||
if (codecs[0]) {
|
||||
radios.add(getRadio('VP8', 'vp8', codec, setCodec));
|
||||
}
|
||||
radios.add(getRadio('VP9', 'vp9', codec, setCodec));
|
||||
if (codecs[1]) {
|
||||
radios.add(getRadio('H264', 'h264', codec, setCodec));
|
||||
}
|
||||
if (codecs[1]) {
|
||||
if (codecs[2]) {
|
||||
radios.add(getRadio('H265', 'h265', codec, setCodec));
|
||||
}
|
||||
radios.add(const Divider(color: MyTheme.border));
|
||||
@ -1071,50 +1096,6 @@ void showOptions(
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}
|
||||
|
||||
void showSetOSPassword(
|
||||
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: close, isOutline: true),
|
||||
dialogButton(
|
||||
'OK',
|
||||
onPressed: () {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||
if (text != "" && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
},
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
void sendPrompt(bool isMac, String key) {
|
||||
final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
|
||||
if (isMac) {
|
||||
|
||||
@ -64,7 +64,7 @@ class InputModel {
|
||||
InputModel(this.parent);
|
||||
|
||||
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
||||
if (!stateGlobal.grabKeyboard) {
|
||||
if (isDesktop && !stateGlobal.grabKeyboard) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
@ -117,44 +117,41 @@ class InputModel {
|
||||
}
|
||||
|
||||
void mapKeyboardMode(RawKeyEvent e) {
|
||||
int scanCode;
|
||||
int keyCode;
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
|
||||
if (e.data is RawKeyEventDataMacOs) {
|
||||
RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs;
|
||||
scanCode = newData.keyCode;
|
||||
keyCode = newData.keyCode;
|
||||
positionCode = newData.keyCode;
|
||||
platformCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataWindows) {
|
||||
RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows;
|
||||
scanCode = newData.scanCode;
|
||||
keyCode = newData.keyCode;
|
||||
positionCode = newData.scanCode;
|
||||
platformCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataLinux) {
|
||||
RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux;
|
||||
// scanCode and keyCode of RawKeyEventDataLinux are incorrect.
|
||||
// 1. scanCode means keycode
|
||||
// 2. keyCode means keysym
|
||||
scanCode = 0;
|
||||
keyCode = newData.scanCode;
|
||||
positionCode = newData.scanCode;
|
||||
platformCode = newData.keyCode;
|
||||
} else if (e.data is RawKeyEventDataAndroid) {
|
||||
RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid;
|
||||
scanCode = newData.scanCode + 8;
|
||||
keyCode = newData.keyCode;
|
||||
} else {
|
||||
scanCode = -1;
|
||||
keyCode = -1;
|
||||
}
|
||||
positionCode = newData.scanCode + 8;
|
||||
platformCode = newData.keyCode;
|
||||
} else {}
|
||||
|
||||
if (e is RawKeyDownEvent) {
|
||||
down = true;
|
||||
} else {
|
||||
down = false;
|
||||
}
|
||||
inputRawKey(e.character ?? '', keyCode, scanCode, down);
|
||||
inputRawKey(e.character ?? '', platformCode, positionCode, down);
|
||||
}
|
||||
|
||||
/// Send raw Key Event
|
||||
void inputRawKey(String name, int keyCode, int scanCode, bool down) {
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
@ -174,8 +171,8 @@ class InputModel {
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
id: id,
|
||||
name: name,
|
||||
keycode: keyCode,
|
||||
scancode: scanCode,
|
||||
platformCode: platformCode,
|
||||
positionCode: positionCode,
|
||||
lockModes: lockModes,
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
@ -293,11 +293,11 @@ class FfiModel with ChangeNotifier {
|
||||
wrongPasswordDialog(id, dialogManager, type, title, text);
|
||||
} else if (type == 'input-password') {
|
||||
enterPasswordDialog(id, dialogManager);
|
||||
} else if (type == 'xsession-login' || type == 'xsession-re-login') {
|
||||
// to-do
|
||||
} else if (type == 'xsession-login-password' ||
|
||||
type == 'xsession-login-password') {
|
||||
// to-do
|
||||
} else if (type == 'session-login' || type == 'session-re-login') {
|
||||
enterUserLoginDialog(id, dialogManager);
|
||||
} else if (type == 'session-login-password' ||
|
||||
type == 'session-login-password') {
|
||||
enterUserLoginAndPasswordDialog(id, dialogManager);
|
||||
} else if (type == 'restarting') {
|
||||
showMsgBox(id, type, title, text, link, false, dialogManager,
|
||||
hasCancel: false);
|
||||
@ -1631,8 +1631,14 @@ class FFI {
|
||||
}
|
||||
|
||||
/// Login with [password], choose if the client should [remember] it.
|
||||
void login(String id, String password, bool remember) {
|
||||
bind.sessionLogin(id: id, password: password, remember: remember);
|
||||
void login(String osUsername, String osPassword, String id, String password,
|
||||
bool remember) {
|
||||
bind.sessionLogin(
|
||||
id: id,
|
||||
osUsername: osUsername,
|
||||
osPassword: osPassword,
|
||||
password: password,
|
||||
remember: remember);
|
||||
}
|
||||
|
||||
/// Close the remote session.
|
||||
@ -1719,6 +1725,7 @@ class PeerInfo {
|
||||
Map<String, dynamic> platform_additions = {};
|
||||
|
||||
bool get is_wayland => platform_additions['is_wayland'] == true;
|
||||
bool get is_headless => platform_additions['headless'] == true;
|
||||
}
|
||||
|
||||
const canvasKey = 'canvas';
|
||||
|
||||
@ -1236,10 +1236,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: texture_rgba_renderer
|
||||
sha256: "52bc9f217b7b07a760ee837d5a17329ad1f78ae8ed1e3fa612c6f1bed3c77f79"
|
||||
sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.13"
|
||||
version: "0.0.16"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#![allow(dead_code)]
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731
|
||||
//
|
||||
// JP/KR mapping https://github.com/TigerVNC/tigervnc/blob/1a008c1380305648ab50f1d99e73439747e9d61d/vncviewer/win32.c#L267
|
||||
|
||||
@ -156,7 +156,7 @@ impl MouseControllable for Enigo {
|
||||
match button {
|
||||
MouseButton::Back => XBUTTON1 as _,
|
||||
MouseButton::Forward => XBUTTON2 as _,
|
||||
_ => 0,
|
||||
_ => 0,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
@ -186,7 +186,7 @@ impl MouseControllable for Enigo {
|
||||
match button {
|
||||
MouseButton::Back => XBUTTON1 as _,
|
||||
MouseButton::Forward => XBUTTON2 as _,
|
||||
_ => 0,
|
||||
_ => 0,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
@ -215,7 +215,7 @@ impl KeyboardControllable for Enigo {
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
fn key_sequence(&mut self, sequence: &str) {
|
||||
let mut buffer = [0; 2];
|
||||
|
||||
@ -247,15 +247,51 @@ impl KeyboardControllable for Enigo {
|
||||
}
|
||||
|
||||
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
||||
let code = self.key_to_keycode(key);
|
||||
if code == 0 || code == 65535 {
|
||||
return Err("".into());
|
||||
}
|
||||
let res = keybd_event(0, code, 0);
|
||||
if res == 0 {
|
||||
let err = get_error();
|
||||
if !err.is_empty() {
|
||||
return Err(err.into());
|
||||
match &key {
|
||||
Key::Layout(c) => {
|
||||
// to-do: dup code
|
||||
// https://github.com/rustdesk/rustdesk/blob/1bc0dd791ed8344997024dc46626bd2ca7df73d2/src/server/input_service.rs#L1348
|
||||
let code = self.get_layoutdependent_keycode(*c);
|
||||
if code as u16 != 0xFFFF {
|
||||
let vk = code & 0x00FF;
|
||||
let flag = code >> 8;
|
||||
let modifiers = [Key::Shift, Key::Control, Key::Alt];
|
||||
let mod_len = modifiers.len();
|
||||
for pos in 0..mod_len {
|
||||
if flag & (0x0001 << pos) != 0 {
|
||||
self.key_down(modifiers[pos])?;
|
||||
}
|
||||
}
|
||||
|
||||
let res = keybd_event(0, vk, 0);
|
||||
let err = if res == 0 { get_error() } else { "".to_owned() };
|
||||
|
||||
for pos in 0..mod_len {
|
||||
let rpos = mod_len - 1 - pos;
|
||||
if flag & (0x0001 << rpos) != 0 {
|
||||
self.key_up(modifiers[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
if !err.is_empty() {
|
||||
return Err(err.into());
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Failed to get keycode of {}", c).into());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let code = self.key_to_keycode(key);
|
||||
if code == 0 || code == 65535 {
|
||||
return Err("".into());
|
||||
}
|
||||
let res = keybd_event(0, code, 0);
|
||||
if res == 0 {
|
||||
let err = get_error();
|
||||
if !err.is_empty() {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -411,30 +447,21 @@ impl Enigo {
|
||||
Key::RightAlt => EVK_RMENU,
|
||||
|
||||
Key::Raw(raw_keycode) => raw_keycode,
|
||||
Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()),
|
||||
Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN,
|
||||
Key::Layout(..) => {
|
||||
// unreachable
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_layoutdependent_keycode(&self, string: String) -> u16 {
|
||||
// get the first char from the string ignore the rest
|
||||
// ensure its not a multybyte char
|
||||
if let Some(chr) = string.chars().nth(0) {
|
||||
// NOTE VkKeyScanW uses the current keyboard LAYOUT
|
||||
// to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout
|
||||
// or load one with LoadKeyboardLayoutW
|
||||
let current_window_thread_id =
|
||||
unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) };
|
||||
unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) };
|
||||
let keycode_and_shiftstate = unsafe { VkKeyScanExW(chr as _, LAYOUT) };
|
||||
if keycode_and_shiftstate == (EVK_DECIMAL as i16) && chr == '.' {
|
||||
// a workaround of italian keyboard shift + '.' issue
|
||||
EVK_PERIOD as _
|
||||
} else {
|
||||
keycode_and_shiftstate as _
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
fn get_layoutdependent_keycode(&self, chr: char) -> u16 {
|
||||
// NOTE VkKeyScanW uses the current keyboard LAYOUT
|
||||
// to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout
|
||||
// or load one with LoadKeyboardLayoutW
|
||||
let current_window_thread_id =
|
||||
unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) };
|
||||
unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) };
|
||||
unsafe { VkKeyScanExW(chr as _, LAYOUT) as _ }
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ message VideoFrame {
|
||||
YUV yuv = 8;
|
||||
EncodedVideoFrames h264s = 10;
|
||||
EncodedVideoFrames h265s = 11;
|
||||
EncodedVideoFrames vp8s = 12;
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +53,11 @@ message FileTransfer {
|
||||
bool show_hidden = 2;
|
||||
}
|
||||
|
||||
message OSLogin {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
bytes password = 2;
|
||||
@ -65,6 +71,7 @@ message LoginRequest {
|
||||
bool video_ack_required = 9;
|
||||
uint64 session_id = 10;
|
||||
string version = 11;
|
||||
OSLogin os_login = 12;
|
||||
}
|
||||
|
||||
message ChatMessage { string text = 1; }
|
||||
@ -76,6 +83,7 @@ message Features {
|
||||
message SupportedEncoding {
|
||||
bool h264 = 1;
|
||||
bool h265 = 2;
|
||||
bool vp8 = 3;
|
||||
}
|
||||
|
||||
message PeerInfo {
|
||||
@ -203,11 +211,13 @@ message KeyEvent {
|
||||
bool press = 2;
|
||||
oneof union {
|
||||
ControlKey control_key = 3;
|
||||
// high word, sym key code. win: virtual-key code, linux: keysym ?, macos:
|
||||
// low word, position key code. win: scancode, linux: key code, macos: key code
|
||||
// position key code. win: scancode, linux: key code, macos: key code
|
||||
uint32 chr = 4;
|
||||
uint32 unicode = 5;
|
||||
string seq = 6;
|
||||
// high word. virtual keycode
|
||||
// low word. unicode
|
||||
uint32 win2win_hotkey = 7;
|
||||
}
|
||||
repeated ControlKey modifiers = 8;
|
||||
KeyboardMode mode = 9;
|
||||
@ -457,18 +467,20 @@ enum ImageQuality {
|
||||
Best = 4;
|
||||
}
|
||||
|
||||
message VideoCodecState {
|
||||
message SupportedDecoding {
|
||||
enum PreferCodec {
|
||||
Auto = 0;
|
||||
VPX = 1;
|
||||
VP9 = 1;
|
||||
H264 = 2;
|
||||
H265 = 3;
|
||||
VP8 = 4;
|
||||
}
|
||||
|
||||
int32 score_vpx = 1;
|
||||
int32 score_h264 = 2;
|
||||
int32 score_h265 = 3;
|
||||
int32 ability_vp9 = 1;
|
||||
int32 ability_h264 = 2;
|
||||
int32 ability_h265 = 3;
|
||||
PreferCodec prefer = 4;
|
||||
int32 ability_vp8 = 5;
|
||||
}
|
||||
|
||||
message OptionMessage {
|
||||
@ -486,7 +498,7 @@ message OptionMessage {
|
||||
BoolOption disable_audio = 7;
|
||||
BoolOption disable_clipboard = 8;
|
||||
BoolOption enable_file_transfer = 9;
|
||||
VideoCodecState video_codec_state = 10;
|
||||
SupportedDecoding supported_decoding = 10;
|
||||
int32 custom_fps = 11;
|
||||
BoolOption disable_keyboard = 12;
|
||||
}
|
||||
|
||||
@ -64,11 +64,15 @@ lazy_static::lazy_static! {
|
||||
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = Default::default();
|
||||
}
|
||||
|
||||
// #[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub const LINK_DOCS_HOME: &str = "https://rustdesk.com/docs/en/";
|
||||
pub const LINK_DOCS_X11_REQUIRED: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required";
|
||||
pub const LINK_HEADLESS_LINUX_SUPPORT: &str =
|
||||
"https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support";
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([
|
||||
("rustdesk docs home", "https://rustdesk.com/docs/en/"),
|
||||
("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
|
||||
("rustdesk docs home", LINK_DOCS_HOME),
|
||||
("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED),
|
||||
("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -915,9 +919,10 @@ impl PeerConfig {
|
||||
decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
|
||||
config.password = password;
|
||||
store = store || store2;
|
||||
for opt in ["rdp_password", "os-password"] {
|
||||
for opt in ["rdp_password", "os-username", "os-password"] {
|
||||
if let Some(v) = config.options.get_mut(opt) {
|
||||
let (encrypted, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
|
||||
let (encrypted, _, store2) =
|
||||
decrypt_str_or_original(v, PASSWORD_ENC_VERSION);
|
||||
*v = encrypted;
|
||||
store = store || store2;
|
||||
}
|
||||
@ -938,7 +943,7 @@ impl PeerConfig {
|
||||
let _lock = CONFIG.read().unwrap();
|
||||
let mut config = self.clone();
|
||||
config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
|
||||
for opt in ["rdp_password", "os-password"] {
|
||||
for opt in ["rdp_password", "os-username", "os-password"] {
|
||||
if let Some(v) = config.options.get_mut(opt) {
|
||||
*v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)
|
||||
}
|
||||
@ -1356,9 +1361,9 @@ impl UserDefaultConfig {
|
||||
"view_style" => self.get_string(key, "original", vec!["adaptive"]),
|
||||
"scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||
"image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]),
|
||||
"codec-preference" => self.get_string(key, "auto", vec!["vp9", "h264", "h265"]),
|
||||
"codec-preference" => self.get_string(key, "auto", vec!["vp8", "vp9", "h264", "h265"]),
|
||||
"custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0),
|
||||
"custom-fps" => self.get_double_string(key, 30.0, 10.0, 120.0),
|
||||
"custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
|
||||
_ => self
|
||||
.options
|
||||
.get(key)
|
||||
|
||||
@ -4,11 +4,15 @@ pub mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use crate::{config::Config, log};
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static mut GLOBAL_CALLBACK: Option<Box<dyn Fn()>> = None;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
let mut stack = vec![];
|
||||
backtrace::trace(|frame| {
|
||||
@ -29,6 +33,16 @@ extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
info = "Always use software rendering will be set.".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
if stack.iter().any(|s| {
|
||||
s.to_lowercase().contains("nvidia")
|
||||
|| s.to_lowercase().contains("amf")
|
||||
|| s.to_lowercase().contains("mfx")
|
||||
|| s.contains("cuProfilerStop")
|
||||
}) {
|
||||
Config::set_option("enable-hwcodec".to_string(), "N".to_string());
|
||||
info = "Perhaps hwcodec causing the crash, disable it first".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
log::error!(
|
||||
"Got signal {} and exit. stack:\n{}",
|
||||
sig,
|
||||
@ -51,6 +65,7 @@ extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn register_breakdown_handler<T>(callback: T)
|
||||
where
|
||||
T: Fn() + 'static,
|
||||
|
||||
@ -3,7 +3,8 @@ use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV};
|
||||
use scrap::{
|
||||
codec::{EncoderApi, EncoderCfg},
|
||||
Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig,
|
||||
VpxVideoCodecId, STRIDE_ALIGN,
|
||||
VpxVideoCodecId::{self, *},
|
||||
STRIDE_ALIGN,
|
||||
};
|
||||
use std::{io::Write, time::Instant};
|
||||
|
||||
@ -49,7 +50,7 @@ fn main() {
|
||||
"benchmark {}x{} bitrate:{}k hw_pixfmt:{:?}",
|
||||
width, height, bitrate_k, args.flag_hw_pixfmt
|
||||
);
|
||||
test_vp9(&yuvs, width, height, bitrate_k, yuv_count);
|
||||
[VP8, VP9].map(|c| test_vpx(c, &yuvs, width, height, bitrate_k, yuv_count));
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
use hwcodec::AVPixelFormat;
|
||||
@ -57,7 +58,7 @@ fn main() {
|
||||
Pixfmt::I420 => AVPixelFormat::AV_PIX_FMT_YUV420P,
|
||||
Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
};
|
||||
let yuvs = hw::vp9_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
|
||||
let yuvs = hw::vpx_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
|
||||
hw::test(&yuvs, width, height, bitrate_k, yuv_count, hw_pixfmt);
|
||||
}
|
||||
}
|
||||
@ -87,13 +88,20 @@ fn capture_yuv(yuv_count: usize) -> (Vec<Vec<u8>>, usize, usize) {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_vp9(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize, yuv_count: usize) {
|
||||
fn test_vpx(
|
||||
codec_id: VpxVideoCodecId,
|
||||
yuvs: &Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
bitrate_k: usize,
|
||||
yuv_count: usize,
|
||||
) {
|
||||
let config = EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: width as _,
|
||||
height: height as _,
|
||||
timebase: [1, 1000],
|
||||
bitrate: bitrate_k as _,
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
codec: codec_id,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
});
|
||||
let mut encoder = VpxEncoder::new(config).unwrap();
|
||||
@ -104,35 +112,43 @@ fn test_vp9(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize,
|
||||
.unwrap();
|
||||
let _ = encoder.flush().unwrap();
|
||||
}
|
||||
println!("vp9 encode: {:?}", start.elapsed() / yuv_count as _);
|
||||
println!(
|
||||
"{:?} encode: {:?}",
|
||||
codec_id,
|
||||
start.elapsed() / yuv_count as _
|
||||
);
|
||||
|
||||
// prepare data separately
|
||||
let mut vp9s = vec![];
|
||||
let mut vpxs = vec![];
|
||||
let start = Instant::now();
|
||||
for yuv in yuvs {
|
||||
for ref frame in encoder
|
||||
.encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN)
|
||||
.unwrap()
|
||||
{
|
||||
vp9s.push(frame.data.to_vec());
|
||||
vpxs.push(frame.data.to_vec());
|
||||
}
|
||||
for ref frame in encoder.flush().unwrap() {
|
||||
vp9s.push(frame.data.to_vec());
|
||||
vpxs.push(frame.data.to_vec());
|
||||
}
|
||||
}
|
||||
assert_eq!(vp9s.len(), yuv_count);
|
||||
assert_eq!(vpxs.len(), yuv_count);
|
||||
|
||||
let mut decoder = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
codec: codec_id,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
let start = Instant::now();
|
||||
for vp9 in vp9s {
|
||||
let _ = decoder.decode(&vp9);
|
||||
for vpx in vpxs {
|
||||
let _ = decoder.decode(&vpx);
|
||||
let _ = decoder.flush();
|
||||
}
|
||||
println!("vp9 decode: {:?}", start.elapsed() / yuv_count as _);
|
||||
println!(
|
||||
"{:?} decode: {:?}",
|
||||
codec_id,
|
||||
start.elapsed() / yuv_count as _
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@ -267,7 +283,7 @@ mod hw {
|
||||
Some(info.clone()) == best.h264 || Some(info.clone()) == best.h265
|
||||
}
|
||||
|
||||
pub fn vp9_yuv_to_hw_yuv(
|
||||
pub fn vpx_yuv_to_hw_yuv(
|
||||
yuvs: Vec<Vec<u8>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
|
||||
@ -50,6 +50,7 @@ impl crate::TraitCapturer for Capturer {
|
||||
|
||||
pub enum Frame<'a> {
|
||||
RAW(&'a [u8]),
|
||||
VP8(&'a [u8]),
|
||||
VP9(&'a [u8]),
|
||||
Empty,
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
@ -11,30 +10,31 @@ use crate::hwcodec::*;
|
||||
use crate::mediacodec::{
|
||||
MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT,
|
||||
};
|
||||
use crate::{vpxcodec::*, ImageFormat};
|
||||
use crate::{vpxcodec::*, CodecName, ImageFormat};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::sysinfo::{System, SystemExt};
|
||||
use hbb_common::{
|
||||
anyhow::anyhow,
|
||||
config::PeerConfig,
|
||||
log,
|
||||
message_proto::{video_frame, EncodedVideoFrames, Message, VideoCodecState},
|
||||
message_proto::{
|
||||
supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message,
|
||||
SupportedDecoding, SupportedEncoding,
|
||||
},
|
||||
ResultType,
|
||||
};
|
||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||
use hbb_common::{
|
||||
config::{Config2, PeerConfig},
|
||||
lazy_static,
|
||||
message_proto::video_codec_state::PreferCodec,
|
||||
};
|
||||
use hbb_common::{config::Config2, lazy_static};
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref PEER_DECODER_STATES: Arc<Mutex<HashMap<i32, VideoCodecState>>> = Default::default();
|
||||
static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
|
||||
static ref CODEC_NAME: Arc<Mutex<CodecName>> = Arc::new(Mutex::new(CodecName::VP9));
|
||||
}
|
||||
const SCORE_VPX: i32 = 90;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HwEncoderConfig {
|
||||
pub codec_name: String,
|
||||
pub name: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub bitrate: i32,
|
||||
@ -58,10 +58,6 @@ pub trait EncoderApi {
|
||||
fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>;
|
||||
}
|
||||
|
||||
pub struct DecoderCfg {
|
||||
pub vpx: VpxDecoderConfig,
|
||||
}
|
||||
|
||||
pub struct Encoder {
|
||||
pub codec: Box<dyn EncoderApi>,
|
||||
}
|
||||
@ -81,7 +77,8 @@ impl DerefMut for Encoder {
|
||||
}
|
||||
|
||||
pub struct Decoder {
|
||||
vpx: VpxDecoder,
|
||||
vp8: VpxDecoder,
|
||||
vp9: VpxDecoder,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hw: HwDecoders,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
@ -91,10 +88,10 @@ pub struct Decoder {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EncoderUpdate {
|
||||
State(VideoCodecState),
|
||||
pub enum EncodingUpdate {
|
||||
New(SupportedDecoding),
|
||||
Remove,
|
||||
DisableHwIfNotExist,
|
||||
NewOnlyVP9,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
@ -111,7 +108,8 @@ impl Encoder {
|
||||
codec: Box::new(hw),
|
||||
}),
|
||||
Err(e) => {
|
||||
check_config_process(true);
|
||||
check_config_process();
|
||||
*CODEC_NAME.lock().unwrap() = CodecName::VP9;
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
@ -120,172 +118,158 @@ impl Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn update_video_encoder(id: i32, update: EncoderUpdate) {
|
||||
pub fn update(id: i32, update: EncodingUpdate) {
|
||||
let mut decodings = PEER_DECODINGS.lock().unwrap();
|
||||
match update {
|
||||
EncodingUpdate::New(decoding) => {
|
||||
decodings.insert(id, decoding);
|
||||
}
|
||||
EncodingUpdate::Remove => {
|
||||
decodings.remove(&id);
|
||||
}
|
||||
EncodingUpdate::NewOnlyVP9 => {
|
||||
decodings.insert(
|
||||
id,
|
||||
SupportedDecoding {
|
||||
ability_vp9: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let vp8_useable = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_vp8 > 0);
|
||||
#[allow(unused_mut)]
|
||||
let mut h264_name = None;
|
||||
#[allow(unused_mut)]
|
||||
let mut h265_name = None;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
let mut states = PEER_DECODER_STATES.lock().unwrap();
|
||||
match update {
|
||||
EncoderUpdate::State(state) => {
|
||||
states.insert(id, state);
|
||||
}
|
||||
EncoderUpdate::Remove => {
|
||||
states.remove(&id);
|
||||
}
|
||||
EncoderUpdate::DisableHwIfNotExist => {
|
||||
if !states.contains_key(&id) {
|
||||
states.insert(id, VideoCodecState::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
let name = HwEncoder::current_name();
|
||||
if states.len() > 0 {
|
||||
if enable_hwcodec_option() {
|
||||
let best = HwEncoder::best();
|
||||
let enabled_h264 = best.h264.is_some()
|
||||
&& states.len() > 0
|
||||
&& states.iter().all(|(_, s)| s.score_h264 > 0);
|
||||
let enabled_h265 = best.h265.is_some()
|
||||
&& states.len() > 0
|
||||
&& states.iter().all(|(_, s)| s.score_h265 > 0);
|
||||
|
||||
// Preference first
|
||||
let mut preference = PreferCodec::Auto;
|
||||
let preferences: Vec<_> = states
|
||||
.iter()
|
||||
.filter(|(_, s)| {
|
||||
s.prefer == PreferCodec::VPX.into()
|
||||
|| s.prefer == PreferCodec::H264.into() && enabled_h264
|
||||
|| s.prefer == PreferCodec::H265.into() && enabled_h265
|
||||
})
|
||||
.map(|(_, s)| s.prefer)
|
||||
.collect();
|
||||
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
|
||||
preference = preferences[0].enum_value_or(PreferCodec::Auto);
|
||||
let h264_useable =
|
||||
decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h264 > 0);
|
||||
let h265_useable =
|
||||
decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h265 > 0);
|
||||
if h264_useable {
|
||||
h264_name = best.h264.map_or(None, |c| Some(c.name));
|
||||
}
|
||||
|
||||
match preference {
|
||||
PreferCodec::VPX => *name.lock().unwrap() = None,
|
||||
PreferCodec::H264 => {
|
||||
*name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name))
|
||||
}
|
||||
PreferCodec::H265 => {
|
||||
*name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name))
|
||||
}
|
||||
PreferCodec::Auto => {
|
||||
// score encoder
|
||||
let mut score_vpx = SCORE_VPX;
|
||||
let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score);
|
||||
let mut score_h265 = best.h265.as_ref().map_or(0, |c| c.score);
|
||||
|
||||
// score decoder
|
||||
score_vpx += states.iter().map(|s| s.1.score_vpx).sum::<i32>();
|
||||
if enabled_h264 {
|
||||
score_h264 += states.iter().map(|s| s.1.score_h264).sum::<i32>();
|
||||
}
|
||||
if enabled_h265 {
|
||||
score_h265 += states.iter().map(|s| s.1.score_h265).sum::<i32>();
|
||||
}
|
||||
|
||||
if enabled_h265 && score_h265 >= score_vpx && score_h265 >= score_h264 {
|
||||
*name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name));
|
||||
} else if enabled_h264
|
||||
&& score_h264 >= score_vpx
|
||||
&& score_h264 >= score_h265
|
||||
{
|
||||
*name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name));
|
||||
} else {
|
||||
*name.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
if h265_useable {
|
||||
h265_name = best.h265.map_or(None, |c| Some(c.name));
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"connection count:{}, used preference:{:?}, encoder:{:?}",
|
||||
states.len(),
|
||||
preference,
|
||||
name.lock().unwrap()
|
||||
)
|
||||
} else {
|
||||
*name.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
{
|
||||
let _ = id;
|
||||
let _ = update;
|
||||
|
||||
let mut name = CODEC_NAME.lock().unwrap();
|
||||
let mut preference = PreferCodec::Auto;
|
||||
let preferences: Vec<_> = decodings
|
||||
.iter()
|
||||
.filter(|(_, s)| {
|
||||
s.prefer == PreferCodec::VP9.into()
|
||||
|| s.prefer == PreferCodec::VP8.into() && vp8_useable
|
||||
|| s.prefer == PreferCodec::H264.into() && h264_name.is_some()
|
||||
|| s.prefer == PreferCodec::H265.into() && h265_name.is_some()
|
||||
})
|
||||
.map(|(_, s)| s.prefer)
|
||||
.collect();
|
||||
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
|
||||
preference = preferences[0].enum_value_or(PreferCodec::Auto);
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn current_hw_encoder_name() -> Option<String> {
|
||||
#[cfg(feature = "hwcodec")]
|
||||
if enable_hwcodec_option() {
|
||||
return HwEncoder::current_name().lock().unwrap().clone();
|
||||
} else {
|
||||
return None;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut auto_codec = CodecName::VP9;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if vp8_useable && System::new_all().total_memory() <= 4 * 1024 * 1024 * 1024 {
|
||||
// 4 Gb
|
||||
auto_codec = CodecName::VP8
|
||||
}
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
return None;
|
||||
|
||||
match preference {
|
||||
PreferCodec::VP8 => *name = CodecName::VP8,
|
||||
PreferCodec::VP9 => *name = CodecName::VP9,
|
||||
PreferCodec::H264 => *name = h264_name.map_or(auto_codec, |c| CodecName::H264(c)),
|
||||
PreferCodec::H265 => *name = h265_name.map_or(auto_codec, |c| CodecName::H265(c)),
|
||||
PreferCodec::Auto => *name = auto_codec,
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"connection count:{}, used preference:{:?}, encoder:{:?}",
|
||||
decodings.len(),
|
||||
preference,
|
||||
*name
|
||||
)
|
||||
}
|
||||
|
||||
pub fn supported_encoding() -> (bool, bool) {
|
||||
#[inline]
|
||||
pub fn negotiated_codec() -> CodecName {
|
||||
CODEC_NAME.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn supported_encoding() -> SupportedEncoding {
|
||||
#[allow(unused_mut)]
|
||||
let mut encoding = SupportedEncoding {
|
||||
vp8: true,
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
if enable_hwcodec_option() {
|
||||
let best = HwEncoder::best();
|
||||
(
|
||||
best.h264.as_ref().map_or(false, |c| c.score > 0),
|
||||
best.h265.as_ref().map_or(false, |c| c.score > 0),
|
||||
)
|
||||
} else {
|
||||
(false, false)
|
||||
encoding.h264 = best.h264.is_some();
|
||||
encoding.h265 = best.h265.is_some();
|
||||
}
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
(false, false)
|
||||
encoding
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
pub fn video_codec_state(_id: &str) -> VideoCodecState {
|
||||
pub fn supported_decodings(id_for_perfer: Option<&str>) -> SupportedDecoding {
|
||||
#[allow(unused_mut)]
|
||||
let mut decoding = SupportedDecoding {
|
||||
ability_vp8: 1,
|
||||
ability_vp9: 1,
|
||||
prefer: id_for_perfer
|
||||
.map_or(PreferCodec::Auto, |id| Self::codec_preference(id))
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(feature = "hwcodec")]
|
||||
if enable_hwcodec_option() {
|
||||
let best = HwDecoder::best();
|
||||
return VideoCodecState {
|
||||
score_vpx: SCORE_VPX,
|
||||
score_h264: best.h264.map_or(0, |c| c.score),
|
||||
score_h265: best.h265.map_or(0, |c| c.score),
|
||||
prefer: Self::codec_preference(_id).into(),
|
||||
..Default::default()
|
||||
};
|
||||
decoding.ability_h264 = if best.h264.is_some() { 1 } else { 0 };
|
||||
decoding.ability_h265 = if best.h265.is_some() { 1 } else { 0 };
|
||||
}
|
||||
#[cfg(feature = "mediacodec")]
|
||||
if enable_hwcodec_option() {
|
||||
let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
92
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let score_h265 = if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
94
|
||||
} else {
|
||||
0
|
||||
};
|
||||
return VideoCodecState {
|
||||
score_vpx: SCORE_VPX,
|
||||
score_h264,
|
||||
score_h265,
|
||||
prefer: Self::codec_preference(_id).into(),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
VideoCodecState {
|
||||
score_vpx: SCORE_VPX,
|
||||
..Default::default()
|
||||
decoding.ability_h264 =
|
||||
if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
decoding.ability_h265 =
|
||||
if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
decoding
|
||||
}
|
||||
|
||||
pub fn new(config: DecoderCfg) -> Decoder {
|
||||
let vpx = VpxDecoder::new(config.vpx).unwrap();
|
||||
pub fn new() -> Decoder {
|
||||
let vp8 = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP8,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
let vp9 = VpxDecoder::new(VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
.unwrap();
|
||||
Decoder {
|
||||
vpx,
|
||||
vp8,
|
||||
vp9,
|
||||
#[cfg(feature = "hwcodec")]
|
||||
hw: if enable_hwcodec_option() {
|
||||
HwDecoder::new_decoders()
|
||||
@ -310,8 +294,11 @@ impl Decoder {
|
||||
rgb: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
match frame {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
Decoder::handle_vpxs_video_frame(&mut self.vp8, vp8s, fmt, rgb)
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, fmt, rgb)
|
||||
Decoder::handle_vpxs_video_frame(&mut self.vp9, vp9s, fmt, rgb)
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
@ -349,15 +336,15 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_vp9s_video_frame(
|
||||
fn handle_vpxs_video_frame(
|
||||
decoder: &mut VpxDecoder,
|
||||
vp9s: &EncodedVideoFrames,
|
||||
vpxs: &EncodedVideoFrames,
|
||||
fmt: (ImageFormat, usize),
|
||||
rgb: &mut Vec<u8>,
|
||||
) -> ResultType<bool> {
|
||||
let mut last_frame = Image::new();
|
||||
for vp9 in vp9s.frames.iter() {
|
||||
for frame in decoder.decode(&vp9.data)? {
|
||||
for vpx in vpxs.frames.iter() {
|
||||
for frame in decoder.decode(&vpx.data)? {
|
||||
drop(last_frame);
|
||||
last_frame = frame;
|
||||
}
|
||||
@ -408,14 +395,15 @@ impl Decoder {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||
fn codec_preference(id: &str) -> PreferCodec {
|
||||
let codec = PeerConfig::load(id)
|
||||
.options
|
||||
.get("codec-preference")
|
||||
.map_or("".to_owned(), |c| c.to_owned());
|
||||
if codec == "vp9" {
|
||||
PreferCodec::VPX
|
||||
if codec == "vp8" {
|
||||
PreferCodec::VP8
|
||||
} else if codec == "vp9" {
|
||||
PreferCodec::VP9
|
||||
} else if codec == "h264" {
|
||||
PreferCodec::H264
|
||||
} else if codec == "h265" {
|
||||
|
||||
@ -7,7 +7,7 @@ use hbb_common::{
|
||||
anyhow::{anyhow, Context},
|
||||
bytes::Bytes,
|
||||
config::HwCodecConfig,
|
||||
lazy_static, log,
|
||||
log,
|
||||
message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame},
|
||||
ResultType,
|
||||
};
|
||||
@ -19,11 +19,6 @@ use hwcodec::{
|
||||
Quality::{self, *},
|
||||
RateControl::{self, *},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HW_ENCODER_NAME: Arc<Mutex<Option<String>>> = Default::default();
|
||||
}
|
||||
|
||||
const CFG_KEY_ENCODER: &str = "bestHwEncoders";
|
||||
const CFG_KEY_DECODER: &str = "bestHwDecoders";
|
||||
@ -49,7 +44,7 @@ impl EncoderApi for HwEncoder {
|
||||
match cfg {
|
||||
EncoderCfg::HW(config) => {
|
||||
let ctx = EncodeContext {
|
||||
name: config.codec_name.clone(),
|
||||
name: config.name.clone(),
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
pixfmt: DEFAULT_PIXFMT,
|
||||
@ -60,12 +55,12 @@ impl EncoderApi for HwEncoder {
|
||||
quality: DEFAULT_HW_QUALITY,
|
||||
rc: DEFAULT_RC,
|
||||
};
|
||||
let format = match Encoder::format_from_name(config.codec_name.clone()) {
|
||||
let format = match Encoder::format_from_name(config.name.clone()) {
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
return Err(anyhow!(format!(
|
||||
"failed to get format from name:{}",
|
||||
config.codec_name
|
||||
config.name
|
||||
)))
|
||||
}
|
||||
};
|
||||
@ -133,10 +128,6 @@ impl HwEncoder {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_name() -> Arc<Mutex<Option<String>>> {
|
||||
HW_ENCODER_NAME.clone()
|
||||
}
|
||||
|
||||
pub fn encode(&mut self, bgra: &[u8]) -> ResultType<Vec<EncodeFrame>> {
|
||||
match self.pixfmt {
|
||||
AVPixelFormat::AV_PIX_FMT_YUV420P => hw::hw_bgra_to_i420(
|
||||
@ -208,7 +199,7 @@ impl HwDecoder {
|
||||
}
|
||||
}
|
||||
if fail {
|
||||
check_config_process(true);
|
||||
check_config_process();
|
||||
}
|
||||
HwDecoders { h264, h265 }
|
||||
}
|
||||
@ -332,13 +323,11 @@ pub fn check_config() {
|
||||
log::error!("Failed to serialize codec info");
|
||||
}
|
||||
|
||||
pub fn check_config_process(force_reset: bool) {
|
||||
pub fn check_config_process() {
|
||||
use hbb_common::sysinfo::{ProcessExt, System, SystemExt};
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if force_reset {
|
||||
HwCodecConfig::remove();
|
||||
}
|
||||
HwCodecConfig::remove();
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(file_name) = exe.file_name().to_owned() {
|
||||
let s = System::new_all();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub use self::vpxcodec::*;
|
||||
use hbb_common::message_proto::{video_frame, VideoFrame};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(quartz)] {
|
||||
@ -92,3 +93,55 @@ pub fn is_cursor_embedded() -> bool {
|
||||
pub fn is_cursor_embedded() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CodecName {
|
||||
VP8,
|
||||
VP9,
|
||||
H264(String),
|
||||
H265(String),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum CodecFormat {
|
||||
VP8,
|
||||
VP9,
|
||||
H264,
|
||||
H265,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&VideoFrame> for CodecFormat {
|
||||
fn from(it: &VideoFrame) -> Self {
|
||||
match it.union {
|
||||
Some(video_frame::Union::Vp8s(_)) => CodecFormat::VP8,
|
||||
Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9,
|
||||
Some(video_frame::Union::H264s(_)) => CodecFormat::H264,
|
||||
Some(video_frame::Union::H265s(_)) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodecName> for CodecFormat {
|
||||
fn from(value: &CodecName) -> Self {
|
||||
match value {
|
||||
CodecName::VP8 => Self::VP8,
|
||||
CodecName::VP9 => Self::VP9,
|
||||
CodecName::H264(_) => Self::H264,
|
||||
CodecName::H265(_) => Self::H265,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for CodecFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
CodecFormat::VP8 => "VP8".into(),
|
||||
CodecFormat::VP9 => "VP9".into(),
|
||||
CodecFormat::H264 => "H264".into(),
|
||||
CodecFormat::H265 => "H265".into(),
|
||||
CodecFormat::Unknown => "Unknow".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::CodecFormat;
|
||||
#[cfg(feature = "hwcodec")]
|
||||
use hbb_common::anyhow::anyhow;
|
||||
use hbb_common::{
|
||||
@ -21,13 +22,6 @@ use webm::mux::{self, Segment, Track, VideoTrack, Writer};
|
||||
|
||||
const MIN_SECS: u64 = 1;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RecordCodecID {
|
||||
VP9,
|
||||
H264,
|
||||
H265,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecorderContext {
|
||||
pub server: bool,
|
||||
@ -36,7 +30,7 @@ pub struct RecorderContext {
|
||||
pub filename: String,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub codec_id: RecordCodecID,
|
||||
pub format: CodecFormat,
|
||||
pub tx: Option<Sender<RecordState>>,
|
||||
}
|
||||
|
||||
@ -55,8 +49,9 @@ impl RecorderContext {
|
||||
}
|
||||
let file = if self.server { "s" } else { "c" }.to_string()
|
||||
+ &self.id.clone()
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string()
|
||||
+ if self.codec_id == RecordCodecID::VP9 {
|
||||
+ &chrono::Local::now().format("_%Y%m%d%H%M%S_").to_string()
|
||||
+ &self.format.to_string()
|
||||
+ if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 {
|
||||
".webm"
|
||||
} else {
|
||||
".mp4"
|
||||
@ -107,8 +102,8 @@ impl DerefMut for Recorder {
|
||||
impl Recorder {
|
||||
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
||||
ctx.set_filename()?;
|
||||
let recorder = match ctx.codec_id {
|
||||
RecordCodecID::VP9 => Recorder {
|
||||
let recorder = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 => Recorder {
|
||||
inner: Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
ctx,
|
||||
},
|
||||
@ -126,8 +121,8 @@ impl Recorder {
|
||||
|
||||
fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> {
|
||||
ctx.set_filename()?;
|
||||
self.inner = match ctx.codec_id {
|
||||
RecordCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
self.inner = match ctx.format {
|
||||
CodecFormat::VP8 | CodecFormat::VP9 => Box::new(WebmRecorder::new(ctx.clone())?),
|
||||
#[cfg(feature = "hwcodec")]
|
||||
_ => Box::new(HwRecorder::new(ctx.clone())?),
|
||||
#[cfg(not(feature = "hwcodec"))]
|
||||
@ -148,10 +143,19 @@ impl Recorder {
|
||||
|
||||
pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> {
|
||||
match frame {
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if self.ctx.codec_id != RecordCodecID::VP9 {
|
||||
video_frame::Union::Vp8s(vp8s) => {
|
||||
if self.ctx.format != CodecFormat::VP8 {
|
||||
self.change(RecorderContext {
|
||||
codec_id: RecordCodecID::VP9,
|
||||
format: CodecFormat::VP8,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
vp8s.frames.iter().map(|f| self.write_video(f)).count();
|
||||
}
|
||||
video_frame::Union::Vp9s(vp9s) => {
|
||||
if self.ctx.format != CodecFormat::VP9 {
|
||||
self.change(RecorderContext {
|
||||
format: CodecFormat::VP9,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
@ -159,25 +163,25 @@ impl Recorder {
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H264s(h264s) => {
|
||||
if self.ctx.codec_id != RecordCodecID::H264 {
|
||||
if self.ctx.format != CodecFormat::H264 {
|
||||
self.change(RecorderContext {
|
||||
codec_id: RecordCodecID::H264,
|
||||
format: CodecFormat::H264,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
if self.ctx.codec_id == RecordCodecID::H264 {
|
||||
if self.ctx.format == CodecFormat::H264 {
|
||||
h264s.frames.iter().map(|f| self.write_video(f)).count();
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hwcodec")]
|
||||
video_frame::Union::H265s(h265s) => {
|
||||
if self.ctx.codec_id != RecordCodecID::H265 {
|
||||
if self.ctx.format != CodecFormat::H265 {
|
||||
self.change(RecorderContext {
|
||||
codec_id: RecordCodecID::H265,
|
||||
format: CodecFormat::H265,
|
||||
..self.ctx.clone()
|
||||
})?;
|
||||
}
|
||||
if self.ctx.codec_id == RecordCodecID::H265 {
|
||||
if self.ctx.format == CodecFormat::H265 {
|
||||
h265s.frames.iter().map(|f| self.write_video(f)).count();
|
||||
}
|
||||
}
|
||||
@ -221,7 +225,11 @@ impl RecorderApi for WebmRecorder {
|
||||
ctx.width as _,
|
||||
ctx.height as _,
|
||||
None,
|
||||
mux::VideoCodecId::VP9,
|
||||
if ctx.format == CodecFormat::VP9 {
|
||||
mux::VideoCodecId::VP9
|
||||
} else {
|
||||
mux::VideoCodecId::VP8
|
||||
},
|
||||
);
|
||||
Ok(WebmRecorder {
|
||||
vt,
|
||||
@ -279,7 +287,7 @@ impl RecorderApi for HwRecorder {
|
||||
filename: ctx.filename.clone(),
|
||||
width: ctx.width,
|
||||
height: ctx.height,
|
||||
is265: ctx.codec_id == RecordCodecID::H265,
|
||||
is265: ctx.format == CodecFormat::H265,
|
||||
framerate: crate::hwcodec::DEFAULT_TIME_BASE[1] as _,
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to create hardware muxer"))?;
|
||||
|
||||
@ -30,6 +30,7 @@ pub struct VpxEncoder {
|
||||
ctx: vpx_codec_ctx_t,
|
||||
width: usize,
|
||||
height: usize,
|
||||
id: VpxVideoCodecId,
|
||||
}
|
||||
|
||||
pub struct VpxDecoder {
|
||||
@ -97,15 +98,10 @@ impl EncoderApi for VpxEncoder {
|
||||
{
|
||||
match cfg {
|
||||
crate::codec::EncoderCfg::VPX(config) => {
|
||||
let i;
|
||||
if cfg!(feature = "VP8") {
|
||||
i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
|
||||
};
|
||||
} else {
|
||||
i = call_vpx_ptr!(vpx_codec_vp9_cx());
|
||||
}
|
||||
let i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
|
||||
};
|
||||
let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
|
||||
call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
|
||||
|
||||
@ -187,12 +183,17 @@ impl EncoderApi for VpxEncoder {
|
||||
VP9E_SET_TILE_COLUMNS as _,
|
||||
4 as c_int
|
||||
));
|
||||
} else if config.codec == VpxVideoCodecId::VP8 {
|
||||
// https://github.com/webmproject/libvpx/blob/972149cafeb71d6f08df89e91a0130d6a38c4b15/vpx/vp8cx.h#L172
|
||||
// https://groups.google.com/a/webmproject.org/g/webm-discuss/c/DJhSrmfQ61M
|
||||
call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 12,));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ctx,
|
||||
width: config.width as _,
|
||||
height: config.height as _,
|
||||
id: config.codec,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("encoder type mismatch")),
|
||||
@ -213,7 +214,7 @@ impl EncoderApi for VpxEncoder {
|
||||
|
||||
// to-do: flush periodically, e.g. 1 second
|
||||
if frames.len() > 0 {
|
||||
Ok(VpxEncoder::create_msg(frames))
|
||||
Ok(VpxEncoder::create_msg(self.id, frames))
|
||||
} else {
|
||||
Err(anyhow!("no valid frame"))
|
||||
}
|
||||
@ -280,13 +281,17 @@ impl VpxEncoder {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn create_msg(vp9s: Vec<EncodedVideoFrame>) -> Message {
|
||||
pub fn create_msg(codec_id: VpxVideoCodecId, frames: Vec<EncodedVideoFrame>) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut vf = VideoFrame::new();
|
||||
vf.set_vp9s(EncodedVideoFrames {
|
||||
frames: vp9s.into(),
|
||||
let vpxs = EncodedVideoFrames {
|
||||
frames: frames.into(),
|
||||
..Default::default()
|
||||
});
|
||||
};
|
||||
match codec_id {
|
||||
VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs),
|
||||
VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs),
|
||||
}
|
||||
msg_out.set_video_frame(vf);
|
||||
msg_out
|
||||
}
|
||||
@ -382,15 +387,10 @@ impl VpxDecoder {
|
||||
pub fn new(config: VpxDecoderConfig) -> Result<Self> {
|
||||
// This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can
|
||||
// cause UB if uninitialized.
|
||||
let i;
|
||||
if cfg!(feature = "VP8") {
|
||||
i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
|
||||
};
|
||||
} else {
|
||||
i = call_vpx_ptr!(vpx_codec_vp9_dx());
|
||||
}
|
||||
let i = match config.codec {
|
||||
VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()),
|
||||
VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()),
|
||||
};
|
||||
let mut ctx = Default::default();
|
||||
let cfg = vpx_codec_dec_cfg_t {
|
||||
threads: if config.num_threads == 0 {
|
||||
|
||||
5
res/pam.d/rustdesk.debian
Normal file
5
res/pam.d/rustdesk.debian
Normal file
@ -0,0 +1,5 @@
|
||||
#%PAM-1.0
|
||||
@include common-auth
|
||||
@include common-account
|
||||
@include common-session
|
||||
@include common-password
|
||||
5
res/pam.d/rustdesk.suse
Normal file
5
res/pam.d/rustdesk.suse
Normal file
@ -0,0 +1,5 @@
|
||||
#%PAM-1.0
|
||||
auth include common-auth
|
||||
account include common-account
|
||||
session include common-session
|
||||
password include common-password
|
||||
130
res/startwm.sh
Executable file
130
res/startwm.sh
Executable file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh.
|
||||
|
||||
#
|
||||
# This script is an example. You might need to edit this script
|
||||
# depending on your distro if it doesn't work for you.
|
||||
#
|
||||
# Uncomment the following line for debug:
|
||||
# exec xterm
|
||||
|
||||
|
||||
# Execution sequence for interactive login shell - pseudocode
|
||||
#
|
||||
# IF /etc/profile is readable THEN
|
||||
# execute ~/.bash_profile
|
||||
# END IF
|
||||
# IF ~/.bash_profile is readable THEN
|
||||
# execute ~/.bash_profile
|
||||
# ELSE
|
||||
# IF ~/.bash_login is readable THEN
|
||||
# execute ~/.bash_login
|
||||
# ELSE
|
||||
# IF ~/.profile is readable THEN
|
||||
# execute ~/.profile
|
||||
# END IF
|
||||
# END IF
|
||||
# END IF
|
||||
pre_start()
|
||||
{
|
||||
if [ -r /etc/profile ]; then
|
||||
. /etc/profile
|
||||
fi
|
||||
if [ -r ~/.bash_profile ]; then
|
||||
. ~/.bash_profile
|
||||
else
|
||||
if [ -r ~/.bash_login ]; then
|
||||
. ~/.bash_login
|
||||
else
|
||||
if [ -r ~/.profile ]; then
|
||||
. ~/.profile
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# When loging out from the interactive shell, the execution sequence is:
|
||||
#
|
||||
# IF ~/.bash_logout exists THEN
|
||||
# execute ~/.bash_logout
|
||||
# END IF
|
||||
post_start()
|
||||
{
|
||||
if [ -r ~/.bash_logout ]; then
|
||||
. ~/.bash_logout
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
#start the window manager
|
||||
wm_start()
|
||||
{
|
||||
if [ -r /etc/default/locale ]; then
|
||||
. /etc/default/locale
|
||||
export LANG LANGUAGE
|
||||
fi
|
||||
|
||||
# debian
|
||||
if [ -r /etc/X11/Xsession ]; then
|
||||
pre_start
|
||||
. /etc/X11/Xsession
|
||||
post_start
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# alpine
|
||||
# Don't use /etc/X11/xinit/Xsession - it doesn't work
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
if [ -f /etc/X11/xinit/xinitrc ]; then
|
||||
pre_start
|
||||
/etc/X11/xinit/xinitrc
|
||||
post_start
|
||||
else
|
||||
echo "** xinit package isn't installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# el
|
||||
if [ -r /etc/X11/xinit/Xsession ]; then
|
||||
pre_start
|
||||
. /etc/X11/xinit/Xsession
|
||||
post_start
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# suse
|
||||
if [ -r /etc/X11/xdm/Xsession ]; then
|
||||
# since the following script run a user login shell,
|
||||
# do not execute the pseudo login shell scripts
|
||||
. /etc/X11/xdm/Xsession
|
||||
exit 0
|
||||
elif [ -r /usr/etc/X11/xdm/Xsession ]; then
|
||||
. /usr/etc/X11/xdm/Xsession
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pre_start
|
||||
xterm
|
||||
post_start
|
||||
}
|
||||
|
||||
#. /etc/environment
|
||||
#export PATH=$PATH
|
||||
#export LANG=$LANG
|
||||
|
||||
# change PATH to be what your environment needs usually what is in
|
||||
# /etc/environment
|
||||
#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"
|
||||
#export PATH=$PATH
|
||||
|
||||
# for PATH and LANG from /etc/environment
|
||||
# pam will auto process the environment file if /etc/pam.d/xrdp-sesman
|
||||
# includes
|
||||
# auth required pam_env.so readenv=1
|
||||
|
||||
wm_start
|
||||
|
||||
exit 1
|
||||
30
res/xorg.conf
Normal file
30
res/xorg.conf
Normal file
@ -0,0 +1,30 @@
|
||||
Section "Monitor"
|
||||
Identifier "Dummy Monitor"
|
||||
|
||||
# Default HorizSync 31.50 - 48.00 kHz
|
||||
HorizSync 5.0 - 150.0
|
||||
# Default VertRefresh 50.00 - 70.00 Hz
|
||||
VertRefresh 5.0 - 100.0
|
||||
|
||||
# Taken from https://www.xpra.org/xorg.conf
|
||||
Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135
|
||||
Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757
|
||||
EndSection
|
||||
|
||||
Section "Device"
|
||||
Identifier "Dummy VideoCard"
|
||||
Driver "dummy"
|
||||
# Default VideoRam 4096
|
||||
# (1920 * 1080 * 4) / 1024 = 8100
|
||||
VideoRam 8100
|
||||
EndSection
|
||||
|
||||
Section "Screen"
|
||||
Identifier "Dummy Screen"
|
||||
Device "Dummy VideoCard"
|
||||
Monitor "Dummy Monitor"
|
||||
SubSectionSub "Display"
|
||||
Depth 24
|
||||
Modes "1920x1080" "1280x720"
|
||||
EndSubSection
|
||||
EndSection
|
||||
34
src/cli.rs
34
src/cli.rs
@ -48,18 +48,24 @@ impl Interface for Session {
|
||||
}
|
||||
|
||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
|
||||
if msgtype == "input-password" {
|
||||
self.sender
|
||||
.send(Data::Login((self.password.clone(), true)))
|
||||
.ok();
|
||||
} else if msgtype == "re-input-password" {
|
||||
log::error!("{}: {}", title, text);
|
||||
let pass = rpassword::prompt_password("Enter password: ").unwrap();
|
||||
self.sender.send(Data::Login((pass, true))).ok();
|
||||
} else if msgtype.contains("error") {
|
||||
log::error!("{}: {}: {}", msgtype, title, text);
|
||||
} else {
|
||||
log::info!("{}: {}: {}", msgtype, title, text);
|
||||
match msgtype {
|
||||
"input-password" => {
|
||||
self.sender
|
||||
.send(Data::Login((self.password.clone(), true)))
|
||||
.ok();
|
||||
}
|
||||
"re-input-password" => {
|
||||
log::error!("{}: {}", title, text);
|
||||
let password = rpassword::prompt_password("Enter password: ").unwrap();
|
||||
let login_data = Data::Login((password, true));
|
||||
self.sender.send(login_data).ok();
|
||||
}
|
||||
msg if msg.contains("error") => {
|
||||
log::error!("{}: {}: {}", msgtype, title, text);
|
||||
}
|
||||
_ => {
|
||||
log::info!("{}: {}: {}", msgtype, title, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,8 +85,8 @@ impl Interface for Session {
|
||||
handle_hash(self.lc.clone(), &pass, hash, self, peer).await;
|
||||
}
|
||||
|
||||
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) {
|
||||
handle_login_from_ui(self.lc.clone(), password, remember, peer).await;
|
||||
async fn handle_login_from_ui(&mut self, os_username: String, os_password: String, password: String, remember: bool, peer: &mut Stream) {
|
||||
handle_login_from_ui(self.lc.clone(), os_username, os_password, password, remember, peer).await;
|
||||
}
|
||||
|
||||
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) {
|
||||
|
||||
279
src/client.rs
279
src/client.rs
@ -3,7 +3,10 @@ use std::{
|
||||
net::SocketAddr,
|
||||
ops::Deref,
|
||||
str::FromStr,
|
||||
sync::{mpsc, Arc, Mutex, RwLock},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc, Arc, Mutex, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
pub use async_trait::async_trait;
|
||||
@ -44,9 +47,9 @@ use hbb_common::{
|
||||
};
|
||||
pub use helper::*;
|
||||
use scrap::{
|
||||
codec::{Decoder, DecoderCfg},
|
||||
codec::Decoder,
|
||||
record::{Recorder, RecorderContext},
|
||||
ImageFormat, VpxDecoderConfig, VpxVideoCodecId,
|
||||
ImageFormat,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@ -917,12 +920,7 @@ impl VideoHandler {
|
||||
/// Create a new video handler.
|
||||
pub fn new() -> Self {
|
||||
VideoHandler {
|
||||
decoder: Decoder::new(DecoderCfg {
|
||||
vpx: VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
},
|
||||
}),
|
||||
decoder: Decoder::new(),
|
||||
rgb: Default::default(),
|
||||
recorder: Default::default(),
|
||||
record: false,
|
||||
@ -954,12 +952,7 @@ impl VideoHandler {
|
||||
|
||||
/// Reset the decoder.
|
||||
pub fn reset(&mut self) {
|
||||
self.decoder = Decoder::new(DecoderCfg {
|
||||
vpx: VpxDecoderConfig {
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: 1,
|
||||
},
|
||||
});
|
||||
self.decoder = Decoder::new();
|
||||
}
|
||||
|
||||
/// Start or stop screen record.
|
||||
@ -973,7 +966,7 @@ impl VideoHandler {
|
||||
filename: "".to_owned(),
|
||||
width: w as _,
|
||||
height: h as _,
|
||||
codec_id: scrap::record::RecordCodecID::VP9,
|
||||
format: scrap::CodecFormat::VP9,
|
||||
tx: None,
|
||||
})
|
||||
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))));
|
||||
@ -999,7 +992,7 @@ pub struct LoginConfigHandler {
|
||||
pub conn_id: i32,
|
||||
features: Option<Features>,
|
||||
session_id: u64,
|
||||
pub supported_encoding: Option<(bool, bool)>,
|
||||
pub supported_encoding: SupportedEncoding,
|
||||
pub restarting_remote_device: bool,
|
||||
pub force_relay: bool,
|
||||
pub direct: Option<bool>,
|
||||
@ -1047,7 +1040,7 @@ impl LoginConfigHandler {
|
||||
self.remember = !config.password.is_empty();
|
||||
self.config = config;
|
||||
self.session_id = rand::random();
|
||||
self.supported_encoding = None;
|
||||
self.supported_encoding = Default::default();
|
||||
self.restarting_remote_device = false;
|
||||
self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay;
|
||||
self.direct = None;
|
||||
@ -1301,11 +1294,12 @@ impl LoginConfigHandler {
|
||||
config.custom_image_quality[0]
|
||||
};
|
||||
msg.custom_image_quality = quality << 8;
|
||||
#[cfg(feature = "flutter")]
|
||||
if let Some(custom_fps) = self.options.get("custom-fps") {
|
||||
msg.custom_fps = custom_fps.parse().unwrap_or(30);
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
if let Some(custom_fps) = self.options.get("custom-fps") {
|
||||
msg.custom_fps = custom_fps.parse().unwrap_or(30);
|
||||
}
|
||||
let view_only = self.get_toggle_option("view-only");
|
||||
if view_only {
|
||||
msg.disable_keyboard = BoolOption::Yes.into();
|
||||
@ -1331,8 +1325,8 @@ impl LoginConfigHandler {
|
||||
msg.disable_clipboard = BoolOption::Yes.into();
|
||||
n += 1;
|
||||
}
|
||||
let state = Decoder::video_codec_state(&self.id);
|
||||
msg.video_codec_state = hbb_common::protobuf::MessageField::some(state);
|
||||
msg.supported_decoding =
|
||||
hbb_common::protobuf::MessageField::some(Decoder::supported_decodings(Some(&self.id)));
|
||||
n += 1;
|
||||
|
||||
if n > 0 {
|
||||
@ -1565,10 +1559,7 @@ impl LoginConfigHandler {
|
||||
self.conn_id = pi.conn_id;
|
||||
// no matter if change, for update file time
|
||||
self.save_config(config);
|
||||
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
|
||||
{
|
||||
self.supported_encoding = Some((pi.encoding.h264, pi.encoding.h265));
|
||||
}
|
||||
self.supported_encoding = pi.encoding.clone().unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn get_remote_dir(&self) -> String {
|
||||
@ -1591,7 +1582,12 @@ impl LoginConfigHandler {
|
||||
}
|
||||
|
||||
/// Create a [`Message`] for login.
|
||||
fn create_login_msg(&self, password: Vec<u8>) -> Message {
|
||||
fn create_login_msg(
|
||||
&self,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: Vec<u8>,
|
||||
) -> Message {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let my_id = Config::get_id_or(crate::common::DEVICE_ID.lock().unwrap().clone());
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@ -1604,6 +1600,12 @@ impl LoginConfigHandler {
|
||||
option: self.get_option_message(true).into(),
|
||||
session_id: self.session_id,
|
||||
version: crate::VERSION.to_string(),
|
||||
os_login: Some(OSLogin {
|
||||
username: os_username,
|
||||
password: os_password,
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
match self.conn_type {
|
||||
@ -1626,10 +1628,10 @@ impl LoginConfigHandler {
|
||||
}
|
||||
|
||||
pub fn change_prefer_codec(&self) -> Message {
|
||||
let state = scrap::codec::Decoder::video_codec_state(&self.id);
|
||||
let decoding = scrap::codec::Decoder::supported_decodings(Some(&self.id));
|
||||
let mut misc = Misc::new();
|
||||
misc.set_option(OptionMessage {
|
||||
video_codec_state: hbb_common::protobuf::MessageField::some(state),
|
||||
supported_decoding: hbb_common::protobuf::MessageField::some(decoding),
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
@ -1679,7 +1681,12 @@ pub type MediaSender = mpsc::Sender<MediaData>;
|
||||
/// * `video_callback` - The callback for video frame. Being called when a video frame is ready.
|
||||
pub fn start_video_audio_threads<F>(
|
||||
video_callback: F,
|
||||
) -> (MediaSender, MediaSender, Arc<ArrayQueue<VideoFrame>>)
|
||||
) -> (
|
||||
MediaSender,
|
||||
MediaSender,
|
||||
Arc<ArrayQueue<VideoFrame>>,
|
||||
Arc<AtomicUsize>,
|
||||
)
|
||||
where
|
||||
F: 'static + FnMut(&mut Vec<u8>) + Send,
|
||||
{
|
||||
@ -1687,21 +1694,48 @@ where
|
||||
let video_queue = Arc::new(ArrayQueue::<VideoFrame>::new(VIDEO_QUEUE_SIZE));
|
||||
let video_queue_cloned = video_queue.clone();
|
||||
let mut video_callback = video_callback;
|
||||
let mut duration = std::time::Duration::ZERO;
|
||||
let mut count = 0;
|
||||
let fps = Arc::new(AtomicUsize::new(0));
|
||||
let decode_fps = fps.clone();
|
||||
let mut skip_beginning = 0;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut video_handler = VideoHandler::new();
|
||||
loop {
|
||||
if let Ok(data) = video_receiver.recv() {
|
||||
match data {
|
||||
MediaData::VideoFrame(vf) => {
|
||||
if let Ok(true) = video_handler.handle_frame(*vf) {
|
||||
MediaData::VideoFrame(_) | MediaData::VideoQueue => {
|
||||
let vf = if let MediaData::VideoFrame(vf) = data {
|
||||
*vf
|
||||
} else {
|
||||
if let Some(vf) = video_queue.pop() {
|
||||
vf
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let start = std::time::Instant::now();
|
||||
if let Ok(true) = video_handler.handle_frame(vf) {
|
||||
video_callback(&mut video_handler.rgb);
|
||||
}
|
||||
}
|
||||
MediaData::VideoQueue => {
|
||||
if let Some(vf) = video_queue.pop() {
|
||||
if let Ok(true) = video_handler.handle_frame(vf) {
|
||||
video_callback(&mut video_handler.rgb);
|
||||
// fps calculation
|
||||
// The first frame will be very slow
|
||||
if skip_beginning < 5 {
|
||||
skip_beginning += 1;
|
||||
continue;
|
||||
}
|
||||
duration += start.elapsed();
|
||||
count += 1;
|
||||
if count % 10 == 0 {
|
||||
fps.store(
|
||||
(count * 1000 / duration.as_millis()) as usize,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
// Clear to get real-time fps
|
||||
if count > 300 {
|
||||
count = 0;
|
||||
duration = Duration::ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1720,7 +1754,7 @@ where
|
||||
log::info!("Video decoder loop exits");
|
||||
});
|
||||
let audio_sender = start_audio_thread();
|
||||
return (video_sender, audio_sender, video_queue_cloned);
|
||||
return (video_sender, audio_sender, video_queue_cloned, decode_fps);
|
||||
}
|
||||
|
||||
/// Start an audio thread
|
||||
@ -1901,6 +1935,71 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) {
|
||||
interface.send(Data::Message(msg_out));
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct LoginErrorMsgBox {
|
||||
msgtype: &'static str,
|
||||
title: &'static str,
|
||||
text: &'static str,
|
||||
link: &'static str,
|
||||
try_again: bool,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref LOGIN_ERROR_MAP: Arc<HashMap<&'static str, LoginErrorMsgBox>> = {
|
||||
use hbb_common::config::LINK_HEADLESS_LINUX_SUPPORT;
|
||||
let map = HashMap::from([(crate::server::LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{
|
||||
msgtype: "session-login",
|
||||
title: "",
|
||||
text: "",
|
||||
link: "",
|
||||
try_again: true,
|
||||
}), (crate::server::LOGIN_MSG_DESKTOP_XSESSION_FAILED, LoginErrorMsgBox{
|
||||
msgtype: "session-re-login",
|
||||
title: "",
|
||||
text: "",
|
||||
link: "",
|
||||
try_again: true,
|
||||
}), (crate::server::LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LoginErrorMsgBox{
|
||||
msgtype: "info-nocancel",
|
||||
title: "another_user_login_title_tip",
|
||||
text: "another_user_login_text_tip",
|
||||
link: "",
|
||||
try_again: false,
|
||||
}), (crate::server::LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, LoginErrorMsgBox{
|
||||
msgtype: "info-nocancel",
|
||||
title: "xorg_not_found_title_tip",
|
||||
text: "xorg_not_found_text_tip",
|
||||
link: LINK_HEADLESS_LINUX_SUPPORT,
|
||||
try_again: true,
|
||||
}), (crate::server::LOGIN_MSG_DESKTOP_NO_DESKTOP, LoginErrorMsgBox{
|
||||
msgtype: "info-nocancel",
|
||||
title: "no_desktop_title_tip",
|
||||
text: "no_desktop_text_tip",
|
||||
link: LINK_HEADLESS_LINUX_SUPPORT,
|
||||
try_again: true,
|
||||
}), (crate::server::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, LoginErrorMsgBox{
|
||||
msgtype: "session-login-password",
|
||||
title: "",
|
||||
text: "",
|
||||
link: "",
|
||||
try_again: true,
|
||||
}), (crate::server::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, LoginErrorMsgBox{
|
||||
msgtype: "session-login-re-password",
|
||||
title: "",
|
||||
text: "",
|
||||
link: "",
|
||||
try_again: true,
|
||||
}), (crate::server::LOGIN_MSG_NO_PASSWORD_ACCESS, LoginErrorMsgBox{
|
||||
msgtype: "wait-remote-accept-nook",
|
||||
title: "Prompt",
|
||||
text: "Please wait for the remote side to accept your session request...",
|
||||
link: "",
|
||||
try_again: true,
|
||||
})]);
|
||||
Arc::new(map)
|
||||
};
|
||||
}
|
||||
|
||||
/// Handle login error.
|
||||
/// Return true if the password is wrong, return false if there's an actual error.
|
||||
pub fn handle_login_error(
|
||||
@ -1908,19 +2007,27 @@ pub fn handle_login_error(
|
||||
err: &str,
|
||||
interface: &impl Interface,
|
||||
) -> bool {
|
||||
if err == "Wrong Password" {
|
||||
if err == crate::server::LOGIN_MSG_PASSWORD_EMPTY {
|
||||
lc.write().unwrap().password = Default::default();
|
||||
interface.msgbox("input-password", "Password Required", "", "");
|
||||
true
|
||||
} else if err == crate::server::LOGIN_MSG_PASSWORD_WRONG {
|
||||
lc.write().unwrap().password = Default::default();
|
||||
interface.msgbox("re-input-password", err, "Do you want to enter again?", "");
|
||||
true
|
||||
} else if err == "No Password Access" {
|
||||
lc.write().unwrap().password = Default::default();
|
||||
interface.msgbox(
|
||||
"wait-remote-accept-nook",
|
||||
"Prompt",
|
||||
"Please wait for the remote side to accept your session request...",
|
||||
"",
|
||||
);
|
||||
true
|
||||
} else if LOGIN_ERROR_MAP.contains_key(err) {
|
||||
if let Some(msgbox_info) = LOGIN_ERROR_MAP.get(err) {
|
||||
interface.msgbox(
|
||||
msgbox_info.msgtype,
|
||||
msgbox_info.title,
|
||||
msgbox_info.text,
|
||||
msgbox_info.link,
|
||||
);
|
||||
msgbox_info.try_again
|
||||
} else {
|
||||
// unreachable!
|
||||
false
|
||||
}
|
||||
} else {
|
||||
if err.contains(SCRAP_X11_REQUIRED) {
|
||||
interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL);
|
||||
@ -1968,16 +2075,21 @@ pub async fn handle_hash(
|
||||
if password.is_empty() {
|
||||
password = lc.read().unwrap().config.password.clone();
|
||||
}
|
||||
if password.is_empty() {
|
||||
let password = if password.is_empty() {
|
||||
// login without password, the remote side can click accept
|
||||
send_login(lc.clone(), Vec::new(), peer).await;
|
||||
interface.msgbox("input-password", "Password Required", "", "");
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&password);
|
||||
hasher.update(&hash.challenge);
|
||||
send_login(lc.clone(), hasher.finalize()[..].into(), peer).await;
|
||||
}
|
||||
hasher.finalize()[..].into()
|
||||
};
|
||||
|
||||
let os_username = lc.read().unwrap().get_option("os-username");
|
||||
let os_password = lc.read().unwrap().get_option("os-password");
|
||||
|
||||
send_login(lc.clone(), os_username, os_password, password, peer).await;
|
||||
lc.write().unwrap().hash = hash;
|
||||
}
|
||||
|
||||
@ -1986,10 +2098,21 @@ pub async fn handle_hash(
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `lc` - Login config.
|
||||
/// * `os_username` - OS username.
|
||||
/// * `os_password` - OS password.
|
||||
/// * `password` - Password.
|
||||
/// * `peer` - [`Stream`] for communicating with peer.
|
||||
async fn send_login(lc: Arc<RwLock<LoginConfigHandler>>, password: Vec<u8>, peer: &mut Stream) {
|
||||
let msg_out = lc.read().unwrap().create_login_msg(password);
|
||||
async fn send_login(
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: Vec<u8>,
|
||||
peer: &mut Stream,
|
||||
) {
|
||||
let msg_out = lc
|
||||
.read()
|
||||
.unwrap()
|
||||
.create_login_msg(os_username, os_password, password);
|
||||
allow_err!(peer.send(&msg_out).await);
|
||||
}
|
||||
|
||||
@ -1998,25 +2121,40 @@ async fn send_login(lc: Arc<RwLock<LoginConfigHandler>>, password: Vec<u8>, peer
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `lc` - Login config.
|
||||
/// * `os_username` - OS username.
|
||||
/// * `os_password` - OS password.
|
||||
/// * `password` - Password.
|
||||
/// * `remember` - Whether to remember password.
|
||||
/// * `peer` - [`Stream`] for communicating with peer.
|
||||
pub async fn handle_login_from_ui(
|
||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: String,
|
||||
remember: bool,
|
||||
peer: &mut Stream,
|
||||
) {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password);
|
||||
hasher.update(&lc.read().unwrap().hash.salt);
|
||||
let res = hasher.finalize();
|
||||
lc.write().unwrap().remember = remember;
|
||||
lc.write().unwrap().password = res[..].into();
|
||||
let mut hash_password = if password.is_empty() {
|
||||
let mut password2 = lc.read().unwrap().password.clone();
|
||||
if password2.is_empty() {
|
||||
password2 = lc.read().unwrap().config.password.clone();
|
||||
}
|
||||
password2
|
||||
} else {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password);
|
||||
hasher.update(&lc.read().unwrap().hash.salt);
|
||||
let res = hasher.finalize();
|
||||
lc.write().unwrap().remember = remember;
|
||||
lc.write().unwrap().password = res[..].into();
|
||||
res[..].into()
|
||||
};
|
||||
let mut hasher2 = Sha256::new();
|
||||
hasher2.update(&res[..]);
|
||||
hasher2.update(&hash_password[..]);
|
||||
hasher2.update(&lc.read().unwrap().hash.challenge);
|
||||
send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await;
|
||||
hash_password = hasher2.finalize()[..].to_vec();
|
||||
|
||||
send_login(lc.clone(), os_username, os_password, hash_password, peer).await;
|
||||
}
|
||||
|
||||
async fn send_switch_login_request(
|
||||
@ -2030,7 +2168,7 @@ async fn send_switch_login_request(
|
||||
lr: hbb_common::protobuf::MessageField::some(
|
||||
lc.read()
|
||||
.unwrap()
|
||||
.create_login_msg(vec![])
|
||||
.create_login_msg("".to_owned(), "".to_owned(), vec![])
|
||||
.login_request()
|
||||
.to_owned(),
|
||||
),
|
||||
@ -2051,7 +2189,14 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
||||
self.msgbox("error", "Error", err, "");
|
||||
}
|
||||
async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream);
|
||||
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);
|
||||
async fn handle_login_from_ui(
|
||||
&mut self,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: String,
|
||||
remember: bool,
|
||||
peer: &mut Stream,
|
||||
);
|
||||
async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream);
|
||||
|
||||
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>>;
|
||||
@ -2071,7 +2216,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
||||
#[derive(Clone)]
|
||||
pub enum Data {
|
||||
Close,
|
||||
Login((String, bool)),
|
||||
Login((String, String, String, bool)),
|
||||
Message(Message),
|
||||
SendFiles((i32, String, String, i32, bool, bool)),
|
||||
RemoveDirAll((i32, String, bool, bool)),
|
||||
|
||||
@ -1,37 +1,8 @@
|
||||
use hbb_common::{
|
||||
get_time,
|
||||
message_proto::{video_frame, Message, VideoFrame, VoiceCallRequest, VoiceCallResponse},
|
||||
message_proto::{Message, VoiceCallRequest, VoiceCallResponse},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum CodecFormat {
|
||||
VP9,
|
||||
H264,
|
||||
H265,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&VideoFrame> for CodecFormat {
|
||||
fn from(it: &VideoFrame) -> Self {
|
||||
match it.union {
|
||||
Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9,
|
||||
Some(video_frame::Union::H264s(_)) => CodecFormat::H264,
|
||||
Some(video_frame::Union::H265s(_)) => CodecFormat::H265,
|
||||
_ => CodecFormat::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for CodecFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
CodecFormat::VP9 => "VP9".into(),
|
||||
CodecFormat::H264 => "H264".into(),
|
||||
CodecFormat::H265 => "H265".into(),
|
||||
CodecFormat::Unknown => "Unknow".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
use scrap::CodecFormat;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct QualityStatus {
|
||||
|
||||
@ -29,10 +29,10 @@ use hbb_common::tokio::{
|
||||
time::{self, Duration, Instant, Interval},
|
||||
};
|
||||
use hbb_common::{allow_err, fs, get_time, log, message_proto::*, Stream};
|
||||
use scrap::CodecFormat;
|
||||
|
||||
use crate::client::{
|
||||
new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1,
|
||||
SEC30,
|
||||
new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::common::{self, update_clipboard};
|
||||
@ -65,6 +65,8 @@ pub struct Remote<T: InvokeUiSession> {
|
||||
frame_count: Arc<AtomicUsize>,
|
||||
video_format: CodecFormat,
|
||||
elevation_requested: bool,
|
||||
fps_control: FpsControl,
|
||||
decode_fps: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl<T: InvokeUiSession> Remote<T> {
|
||||
@ -76,6 +78,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
receiver: mpsc::UnboundedReceiver<Data>,
|
||||
sender: mpsc::UnboundedSender<Data>,
|
||||
frame_count: Arc<AtomicUsize>,
|
||||
decode_fps: Arc<AtomicUsize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
handler,
|
||||
@ -100,6 +103,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
stop_voice_call_sender: None,
|
||||
voice_call_request_timestamp: None,
|
||||
elevation_requested: false,
|
||||
fps_control: Default::default(),
|
||||
decode_fps,
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +152,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let mut rx_clip_client = rx_clip_client_lock.lock().await;
|
||||
|
||||
let mut status_timer = time::interval(Duration::new(1, 0));
|
||||
let mut fps_instant = Instant::now();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@ -224,9 +230,18 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
_ = status_timer.tick() => {
|
||||
let speed = self.data_count.swap(0, Ordering::Relaxed);
|
||||
self.fps_control();
|
||||
let elapsed = fps_instant.elapsed().as_millis();
|
||||
if elapsed < 1000 {
|
||||
continue;
|
||||
}
|
||||
fps_instant = Instant::now();
|
||||
let mut speed = self.data_count.swap(0, Ordering::Relaxed);
|
||||
speed = speed * 1000 / elapsed as usize;
|
||||
let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32);
|
||||
let fps = self.frame_count.swap(0, Ordering::Relaxed) as _;
|
||||
let mut fps = self.frame_count.swap(0, Ordering::Relaxed) as _;
|
||||
// Correcting the inaccuracy of status_timer
|
||||
fps = fps * 1000 / elapsed as i32;
|
||||
self.handler.update_quality_status(QualityStatus {
|
||||
speed:Some(speed),
|
||||
fps:Some(fps),
|
||||
@ -360,9 +375,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
allow_err!(peer.send(&msg).await);
|
||||
return false;
|
||||
}
|
||||
Data::Login((password, remember)) => {
|
||||
Data::Login((os_username, os_password, password, remember)) => {
|
||||
self.handler
|
||||
.handle_login_from_ui(password, remember, peer)
|
||||
.handle_login_from_ui(os_username, os_password, password, remember, peer)
|
||||
.await;
|
||||
}
|
||||
Data::ToggleClipboardFile => {
|
||||
@ -817,16 +832,62 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
fn contains_key_frame(vf: &VideoFrame) -> bool {
|
||||
use video_frame::Union::*;
|
||||
match &vf.union {
|
||||
Some(vf) => match vf {
|
||||
video_frame::Union::Vp9s(f) => f.frames.iter().any(|e| e.key),
|
||||
video_frame::Union::H264s(f) => f.frames.iter().any(|e| e.key),
|
||||
video_frame::Union::H265s(f) => f.frames.iter().any(|e| e.key),
|
||||
Vp8s(f) | Vp9s(f) | H264s(f) | H265s(f) => f.frames.iter().any(|e| e.key),
|
||||
_ => false,
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn fps_control(&mut self) {
|
||||
let len = self.video_queue.len();
|
||||
let ctl = &mut self.fps_control;
|
||||
// Current full speed decoding fps
|
||||
let decode_fps = self.decode_fps.load(std::sync::atomic::Ordering::Relaxed);
|
||||
// 500ms
|
||||
let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 };
|
||||
if len < debounce || decode_fps == 0 {
|
||||
return;
|
||||
}
|
||||
let mut refresh = false;
|
||||
// First setting , or the length of the queue still increases after setting, or exceed the size of the last setting again
|
||||
if ctl.set_times < 10 // enough
|
||||
&& (ctl.set_times == 0
|
||||
|| (len > ctl.last_queue_size && ctl.last_set_instant.elapsed().as_secs() > 30))
|
||||
{
|
||||
// 80% fps to ensure decoding is faster than encoding
|
||||
let mut custom_fps = decode_fps as i32 * 4 / 5;
|
||||
if custom_fps < 1 {
|
||||
custom_fps = 1;
|
||||
}
|
||||
// send custom fps
|
||||
let mut misc = Misc::new();
|
||||
misc.set_option(OptionMessage {
|
||||
custom_fps,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg = Message::new();
|
||||
msg.set_misc(misc);
|
||||
self.sender.send(Data::Message(msg)).ok();
|
||||
ctl.last_queue_size = len;
|
||||
ctl.set_times += 1;
|
||||
ctl.last_set_instant = Instant::now();
|
||||
refresh = true;
|
||||
}
|
||||
// send refresh
|
||||
if ctl.refresh_times < 10 // enough
|
||||
&& (refresh
|
||||
|| (len > self.video_queue.len() / 2
|
||||
&& ctl.last_refresh_instant.elapsed().as_secs() > 30))
|
||||
{
|
||||
self.handler.refresh_video();
|
||||
ctl.refresh_times += 1;
|
||||
ctl.last_refresh_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&data) {
|
||||
@ -1256,6 +1317,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
},
|
||||
Some(message::Union::MessageBox(msgbox)) => {
|
||||
let mut link = msgbox.link;
|
||||
// Links from the remote side must be verified.
|
||||
if !link.starts_with("rustdesk://") {
|
||||
if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) {
|
||||
link = v.to_string();
|
||||
@ -1489,3 +1551,23 @@ impl RemoveJob {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FpsControl {
|
||||
last_queue_size: usize,
|
||||
set_times: usize,
|
||||
refresh_times: usize,
|
||||
last_set_instant: Instant,
|
||||
last_refresh_instant: Instant,
|
||||
}
|
||||
|
||||
impl Default for FpsControl {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_queue_size: Default::default(),
|
||||
set_times: Default::default(),
|
||||
refresh_times: Default::default(),
|
||||
last_set_instant: Instant::now(),
|
||||
last_refresh_instant: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -818,3 +818,21 @@ pub async fn get_key(sync: bool) -> String {
|
||||
}
|
||||
key
|
||||
}
|
||||
|
||||
pub fn is_peer_version_ge(v: &str) -> bool {
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
if let Some(session) = crate::ui::CUR_SESSION.lock().unwrap().as_ref() {
|
||||
return session.get_peer_version() >= hbb_common::get_version_number(v);
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
if let Some(session) = crate::flutter::SESSIONS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&*crate::flutter::CUR_SESSION_ID.read().unwrap())
|
||||
{
|
||||
return session.get_peer_version() >= hbb_common::get_version_number(v);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::platform::breakdown_callback;
|
||||
use hbb_common::log;
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::platform::register_breakdown_handler;
|
||||
|
||||
@ -39,6 +42,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
register_breakdown_handler(breakdown_callback);
|
||||
#[cfg(target_os = "linux")]
|
||||
@ -224,6 +228,11 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
// call connection manager to establish connections
|
||||
// meanwhile, return true to call flutter window to show control panel
|
||||
crate::ui_interface::start_option_status_sync();
|
||||
} else if args[0] == "--cm-no-ui" {
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::flutter::connection_manager::start_cm_no_ui();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
//_async_logger_holder.map(|x| x.flush());
|
||||
|
||||
@ -218,7 +218,7 @@ impl VideoRenderer {
|
||||
}
|
||||
|
||||
pub fn on_rgba(&self, rgba: &Vec<u8>) {
|
||||
if self.ptr == usize::default() {
|
||||
if self.ptr == usize::default() || self.width == 0 || self.height == 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(func) = &self.on_rgba_func {
|
||||
@ -815,8 +815,20 @@ pub mod connection_manager {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn start_cm_no_ui() {
|
||||
start_listen_ipc(false);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn start_listen_ipc_thread() {
|
||||
start_listen_ipc(true);
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn start_listen_ipc(new_thread: bool) {
|
||||
use crate::ui_cm_interface::{start_ipc, ConnectionManager};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@ -825,7 +837,11 @@ pub mod connection_manager {
|
||||
let cm = ConnectionManager {
|
||||
ui_handler: FlutterHandler {},
|
||||
};
|
||||
std::thread::spawn(move || start_ipc(cm));
|
||||
if new_thread {
|
||||
std::thread::spawn(move || start_ipc(cm));
|
||||
} else {
|
||||
start_ipc(cm);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
|
||||
@ -136,9 +136,15 @@ pub fn session_get_option(id: String, arg: String) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_login(id: String, password: String, remember: bool) {
|
||||
pub fn session_login(
|
||||
id: String,
|
||||
os_username: String,
|
||||
os_password: String,
|
||||
password: String,
|
||||
remember: bool,
|
||||
) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.login(password, remember);
|
||||
session.login(os_username, os_password, password, remember);
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,13 +332,13 @@ pub fn session_switch_display(id: String, value: i32) {
|
||||
pub fn session_handle_flutter_key_event(
|
||||
id: String,
|
||||
name: String,
|
||||
keycode: i32,
|
||||
scancode: i32,
|
||||
platform_code: i32,
|
||||
position_code: i32,
|
||||
lock_modes: i32,
|
||||
down_or_up: bool,
|
||||
) {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
session.handle_flutter_key_event(&name, keycode, scancode, lock_modes, down_or_up);
|
||||
session.handle_flutter_key_event(&name, platform_code, position_code, lock_modes, down_or_up);
|
||||
}
|
||||
}
|
||||
|
||||
@ -969,6 +975,13 @@ pub fn main_has_hwcodec() -> SyncReturn<bool> {
|
||||
SyncReturn(has_hwcodec())
|
||||
}
|
||||
|
||||
pub fn main_supported_hwdecodings() -> SyncReturn<String> {
|
||||
let decoding = supported_hwdecodings();
|
||||
let msg = HashMap::from([("h264", decoding.0), ("h265", decoding.1)]);
|
||||
|
||||
SyncReturn(serde_json::ser::to_string(&msg).unwrap_or("".to_owned()))
|
||||
}
|
||||
|
||||
pub fn main_is_root() -> bool {
|
||||
is_root()
|
||||
}
|
||||
@ -1054,10 +1067,10 @@ pub fn session_send_note(id: String, note: String) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_supported_hwcodec(id: String) -> String {
|
||||
pub fn session_alternative_codecs(id: String) -> String {
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
let (h264, h265) = session.supported_hwcodec();
|
||||
let msg = HashMap::from([("h264", h264), ("h265", h265)]);
|
||||
let (vp8, h264, h265) = session.alternative_codecs();
|
||||
let msg = HashMap::from([("vp8", vp8), ("h264", h264), ("h265", h265)]);
|
||||
serde_json::ser::to_string(&msg).unwrap_or("".to_owned())
|
||||
} else {
|
||||
String::new()
|
||||
|
||||
112
src/keyboard.rs
112
src/keyboard.rs
@ -4,7 +4,7 @@ use crate::common::GrabState;
|
||||
#[cfg(feature = "flutter")]
|
||||
use crate::flutter::{CUR_SESSION_ID, SESSIONS};
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::platform::windows::get_char_by_vk;
|
||||
use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk};
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
use crate::ui::CUR_SESSION;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@ -339,24 +339,8 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode {
|
||||
"translate" => KeyboardMode::Translate,
|
||||
"legacy" => KeyboardMode::Legacy,
|
||||
_ => {
|
||||
// Set "map" as default mode if version > 1.2.0.
|
||||
let mut is_peer_version_gt_1_2_0 = false;
|
||||
|
||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||
is_peer_version_gt_1_2_0 =
|
||||
session.get_peer_version() > hbb_common::get_version_number("1.2.0");
|
||||
}
|
||||
#[cfg(feature = "flutter")]
|
||||
if let Some(session) = SESSIONS
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&*CUR_SESSION_ID.read().unwrap())
|
||||
{
|
||||
is_peer_version_gt_1_2_0 =
|
||||
session.get_peer_version() > hbb_common::get_version_number("1.2.0");
|
||||
}
|
||||
if is_peer_version_gt_1_2_0 {
|
||||
// Set "map" as default mode if version >= 1.2.0.
|
||||
if crate::is_peer_version_ge("1.2.0") {
|
||||
KeyboardMode::Map
|
||||
} else {
|
||||
KeyboardMode::Legacy
|
||||
@ -365,6 +349,21 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_modifier(key: &rdev::Key) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
Key::ShiftLeft
|
||||
| Key::ShiftRight
|
||||
| Key::ControlLeft
|
||||
| Key::ControlRight
|
||||
| Key::MetaLeft
|
||||
| Key::MetaRight
|
||||
| Key::Alt
|
||||
| Key::AltGr
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn is_numpad_key(event: &Event) -> bool {
|
||||
@ -850,6 +849,7 @@ fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &m
|
||||
if name.len() > 0 {
|
||||
let mut evt = key_event.clone();
|
||||
evt.set_seq(name.to_string());
|
||||
evt.down = true;
|
||||
events.push(evt);
|
||||
}
|
||||
}
|
||||
@ -859,9 +859,10 @@ fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &m
|
||||
#[cfg(target_os = "windows")]
|
||||
if _peer == OS_LOWER_LINUX {
|
||||
if is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } {
|
||||
if let Some(chr) = get_char_by_vk(event.platform_code as u32) {
|
||||
if let Some(chr) = get_char_from_vk(event.platform_code as u32) {
|
||||
let mut evt = key_event.clone();
|
||||
evt.set_seq(chr.to_string());
|
||||
evt.down = true;
|
||||
events.push(evt);
|
||||
}
|
||||
}
|
||||
@ -870,6 +871,36 @@ fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &m
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn try_file_win2win_hotkey(
|
||||
peer: &str,
|
||||
event: &Event,
|
||||
key_event: &KeyEvent,
|
||||
events: &mut Vec<KeyEvent>,
|
||||
) {
|
||||
if peer == OS_LOWER_WINDOWS && is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } {
|
||||
let mut down = false;
|
||||
let win2win_hotkey = match event.event_type {
|
||||
EventType::KeyPress(..) => {
|
||||
down = true;
|
||||
if let Some(unicode) = get_unicode_from_vk(event.platform_code as u32) {
|
||||
Some((unicode as u32 & 0x0000FFFF) | (event.platform_code << 16))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
EventType::KeyRelease(..) => Some(event.platform_code << 16),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(code) = win2win_hotkey {
|
||||
let mut evt = key_event.clone();
|
||||
evt.set_win2win_hotkey(code);
|
||||
evt.down = down;
|
||||
events.push(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn is_hot_key_modifiers_down() -> bool {
|
||||
if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) {
|
||||
@ -884,25 +915,6 @@ fn is_hot_key_modifiers_down() -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option<KeyEvent> {
|
||||
let mut key_event = map_keyboard_mode(peer, event, key_event)?;
|
||||
let chr = if peer == OS_LOWER_WINDOWS {
|
||||
(key_event.chr() & 0x0000FFFF) | ((event.platform_code as u32) << 16)
|
||||
} else {
|
||||
key_event.chr()
|
||||
};
|
||||
key_event.set_chr(chr);
|
||||
Some(key_event)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option<KeyEvent> {
|
||||
map_keyboard_mode(peer, event, key_event)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
fn is_altgr(event: &Event) -> bool {
|
||||
@ -927,8 +939,10 @@ fn is_press(event: &Event) -> bool {
|
||||
matches!(event.event_type, EventType::KeyPress(_))
|
||||
}
|
||||
|
||||
// https://github.com/fufesou/rustdesk/wiki/Keyboard-mode----Translate-Mode
|
||||
pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
|
||||
let mut events: Vec<KeyEvent> = Vec::new();
|
||||
|
||||
if let Some(unicode_info) = &event.unicode {
|
||||
if unicode_info.is_dead {
|
||||
#[cfg(target_os = "macos")]
|
||||
@ -945,7 +959,7 @@ pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if is_numpad_key(&event) {
|
||||
if let Some(evt) = translate_key_code(peer, event, key_event) {
|
||||
if let Some(evt) = map_keyboard_mode(peer, event, key_event) {
|
||||
events.push(evt);
|
||||
}
|
||||
return events;
|
||||
@ -967,11 +981,15 @@ pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -
|
||||
return events;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
try_file_win2win_hotkey(peer, event, &key_event, &mut events);
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
if is_press(event) {
|
||||
if events.is_empty() && is_press(event) {
|
||||
try_fill_unicode(peer, event, &key_event, &mut events);
|
||||
}
|
||||
|
||||
// If AltGr is down, no need to send events other than unicode.
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
if IS_0X021D_DOWN {
|
||||
@ -985,9 +1003,19 @@ pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -
|
||||
}
|
||||
|
||||
if events.is_empty() {
|
||||
if let Some(evt) = translate_key_code(peer, event, key_event) {
|
||||
if let Some(evt) = map_keyboard_mode(peer, event, key_event) {
|
||||
events.push(evt);
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn keycode_to_rdev_key(keycode: u32) -> Key {
|
||||
#[cfg(target_os = "windows")]
|
||||
return rdev::win_key_from_scancode(keycode);
|
||||
#[cfg(target_os = "linux")]
|
||||
return rdev::linux_key_from_code(keycode);
|
||||
#[cfg(target_os = "macos")]
|
||||
return rdev::macos_key_from_code(keycode.try_into().unwrap_or_default());
|
||||
}
|
||||
|
||||
@ -5,12 +5,12 @@ mod cn;
|
||||
mod cs;
|
||||
mod da;
|
||||
mod de;
|
||||
mod el;
|
||||
mod en;
|
||||
mod eo;
|
||||
mod es;
|
||||
mod fa;
|
||||
mod fr;
|
||||
mod el;
|
||||
mod hu;
|
||||
mod id;
|
||||
mod it;
|
||||
@ -32,6 +32,7 @@ mod tr;
|
||||
mod tw;
|
||||
mod ua;
|
||||
mod vn;
|
||||
mod lt;
|
||||
|
||||
pub const LANGS: &[(&str, &str)] = &[
|
||||
("en", "English"),
|
||||
@ -66,6 +67,7 @@ pub const LANGS: &[(&str, &str)] = &[
|
||||
("th", "ภาษาไทย"),
|
||||
("sl", "Slovenščina"),
|
||||
("ro", "Română"),
|
||||
("lt", "Lietuvių"),
|
||||
];
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@ -129,6 +131,7 @@ pub fn translate_locale(name: String, locale: &str) -> String {
|
||||
"th" => th::T.deref(),
|
||||
"sl" => sl::T.deref(),
|
||||
"ro" => ro::T.deref(),
|
||||
"lt" => lt::T.deref(),
|
||||
_ => en::T.deref(),
|
||||
};
|
||||
if let Some(v) = m.get(&name as &str) {
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "此文件与对方的一致"),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", "浏览模式"),
|
||||
("enter_rustdesk_passwd_tip", "请输入 RustDesk 密码"),
|
||||
("remember_rustdesk_passwd_tip", "记住 RustDesk 密码"),
|
||||
("login_linux_tip", "登录被控端的 Linux 账户"),
|
||||
("login_linux_tooltip_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"),
|
||||
("login_linux_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"),
|
||||
("verify_rustdesk_password_tip", "验证 RustDesk 密码"),
|
||||
("remember_account_tip", "记住此账户"),
|
||||
("os_account_desk_tip", "在无显示器的环境下,此账户用于登录被控系统,并启用桌面"),
|
||||
("OS Account", "系统账户"),
|
||||
("another_user_login_title_tip", "其他用户已登录"),
|
||||
("another_user_login_text_tip", "断开"),
|
||||
("xorg_not_found_title_tip", "Xorg 未安装"),
|
||||
("xorg_not_found_text_tip", "请安装 Xorg"),
|
||||
("no_desktop_title_tip", "desktop 未安装"),
|
||||
("no_desktop_text_tip", "请安装 desktop"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."),
|
||||
("show_monitors_tip", "Monitore in der Symbolleiste anzeigen"),
|
||||
("View Mode", "Ansichtsmodus"),
|
||||
("enter_rustdesk_passwd_tip", "RustDesk-Passwort eingeben."),
|
||||
("remember_rustdesk_passwd_tip", "RustDesk-Passwort merken."),
|
||||
("login_linux_tip", "Anmeldung am entfernten Linux-Konto"),
|
||||
("login_linux_tooltip_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."),
|
||||
("login_linux_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."),
|
||||
("verify_rustdesk_password_tip", "RustDesk-Passwort bestätigen"),
|
||||
("remember_account_tip", "Dieses Konto merken"),
|
||||
("os_account_desk_tip", "Dieses Konto wird verwendet, um sich beim entfernten Betriebssystem anzumelden und die Desktop-Sitzung im Headless-Modus zu aktivieren."),
|
||||
("OS Account", "Betriebssystem-Konto"),
|
||||
("another_user_login_title_tip", "Ein anderer Benutzer ist bereits angemeldet."),
|
||||
("another_user_login_text_tip", "Trennen"),
|
||||
("xorg_not_found_title_tip", "Xorg nicht gefunden."),
|
||||
("xorg_not_found_text_tip", "Bitte installieren Sie Xorg."),
|
||||
("no_desktop_title_tip", "Es ist kein Desktop verfügbar."),
|
||||
("no_desktop_text_tip", "Bitte installieren Sie den GNOME-Desktop."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."),
|
||||
("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"),
|
||||
("View Mode", "Λειτουργία προβολής"),
|
||||
("enter_rustdesk_passwd_tip", "Εισαγωγή του κωδικού RustDesk."),
|
||||
("remember_rustdesk_passwd_tip", "Να θυμάσαι τον κωδικό του RustDesk."),
|
||||
("login_linux_tip", "Είσοδος σε απομακρυσμένο λογαριασμό Linux"),
|
||||
("login_linux_tooltip_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."),
|
||||
("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -54,9 +54,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."),
|
||||
("identical_file_tip", "This file is identical with the peer's one."),
|
||||
("show_monitors_tip", "Show monitors in toolbar."),
|
||||
("enter_rustdesk_passwd_tip", "Enter RustDesk password."),
|
||||
("remember_rustdesk_passwd_tip", "Remember RustDesk password."),
|
||||
("login_linux_tip", "Login to remote Linux account"),
|
||||
("login_linux_tooltip_tip", "You need to login to remote Linux account to enable a X desktop session."),
|
||||
("enter_rustdesk_passwd_tip", "Enter RustDesk password"),
|
||||
("remember_rustdesk_passwd_tip", "Remember RustDesk password"),
|
||||
("login_linux_tip", "You need to login to remote Linux account to enable a X desktop session"),
|
||||
("verify_rustdesk_password_tip", "Verify RustDesk password"),
|
||||
("remember_account_tip", "Remember this account"),
|
||||
("os_account_desk_tip", "This account is used to login the remote OS and enable the desktop session in headless"),
|
||||
("another_user_login_title_tip", "Another user already logged in"),
|
||||
("another_user_login_text_tip", "Disconnect"),
|
||||
("xorg_not_found_title_tip", "Xorg not found"),
|
||||
("xorg_not_found_text_tip", "Please install Xorg"),
|
||||
("no_desktop_title_tip", "No desktop is avaliable"),
|
||||
("no_desktop_text_tip", "Please install GNOME desktop"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Este archivo es idéntico al del par."),
|
||||
("show_monitors_tip", "Mostrar monitores en la barra de herramientas"),
|
||||
("View Mode", "Modo Vista"),
|
||||
("enter_rustdesk_passwd_tip", "Introduzca la contraseña de RustDesk"),
|
||||
("remember_rustdesk_passwd_tip", "Recordar la contraseña de RustDesk"),
|
||||
("login_linux_tip", "Iniciar sesión para la cuenta remota de Linux"),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -479,9 +479,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Me", "من"),
|
||||
("identical_file_tip", "این فایل با فایل همتا یکسان است."),
|
||||
("show_monitors_tip", "نمایش مانیتورها در نوار ابزار"),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("View Mode", "حالت مشاهده"),
|
||||
("login_linux_tip", "برای فعال کردن دسکتاپ X، باید به حساب لینوکس راه دور وارد شوید"),
|
||||
("verify_rustdesk_password_tip", "رمز عبور RustDesk را تأیید کنید"),
|
||||
("remember_account_tip", "این حساب را به خاطر بسپارید"),
|
||||
("os_account_desk_tip", "این حساب برای ورود به سیستم عامل راه دور و فعال کردن جلسه دسکتاپ در هدلس استفاده می شود"),
|
||||
("OS Account", "حساب کاربری سیستم عامل"),
|
||||
("another_user_login_title_tip", "کاربر دیگری قبلاً وارد شده است"),
|
||||
("another_user_login_text_tip", "قطع شدن"),
|
||||
("xorg_not_found_title_tip", "پیدا نشد Xorg"),
|
||||
("xorg_not_found_text_tip", "لطفا Xorg را نصب کنید"),
|
||||
("no_desktop_title_tip", "هیچ دسکتاپی در دسترس نیست"),
|
||||
("no_desktop_text_tip", "لطفا دسکتاپ گنوم را نصب کنید"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -213,7 +213,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Closed manually by the peer", "Fermé manuellement par le pair"),
|
||||
("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"),
|
||||
("Run without install", "Exécuter sans installer"),
|
||||
("Connect via relay", ""),
|
||||
("Connect via relay", "Connexion via relais"),
|
||||
("Always connect via relay", "Forcer la connexion relais"),
|
||||
("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"),
|
||||
("Login", "Connexion"),
|
||||
@ -288,8 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("android_service_will_start_tip", "L'activation de la capture d'écran démarrera automatiquement le service, permettant à d'autres appareils de demander une connexion à partir de cet appareil."),
|
||||
("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."),
|
||||
("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."),
|
||||
("android_start_service_tip", ""),
|
||||
("android_permission_may_not_change_tip", ""),
|
||||
("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l'autorisation [Capture d'écran] pour démarrer le service de partage d'écran."),
|
||||
("android_permission_may_not_change_tip", "Les autorisations pour les connexions établies peuvent ne pas être prisent en compte instantanément ou avant la reconnection."),
|
||||
("Account", "Compte"),
|
||||
("Overwrite", "Écraser"),
|
||||
("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"),
|
||||
@ -311,8 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"),
|
||||
("Ignore Battery Optimizations", "Ignorer les optimisations batterie"),
|
||||
("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"),
|
||||
("Start on Boot", ""),
|
||||
("Start the screen sharing service on boot, requires special permissions", ""),
|
||||
("Start on Boot", "Lancer au démarrage"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d'écran au démarrage, nécessite des autorisations spéciales"),
|
||||
("Connection not allowed", "Connexion non autorisée"),
|
||||
("Legacy mode", "Mode hérité"),
|
||||
("Map mode", ""),
|
||||
@ -348,7 +348,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Security", "Sécurité"),
|
||||
("Theme", "Thème"),
|
||||
("Dark Theme", "Thème somble"),
|
||||
("Light Theme", ""),
|
||||
("Light Theme", "Thème clair"),
|
||||
("Dark", "Sombre"),
|
||||
("Light", "Clair"),
|
||||
("Follow System", "Suivi système"),
|
||||
@ -421,7 +421,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."),
|
||||
("Always use software rendering", "Utiliser toujours le rendu logiciel"),
|
||||
("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."),
|
||||
("config_microphone", ""),
|
||||
("config_microphone", "Pour discuter à distance, vous devez accorder à RustDesk les autorisations « Enregistrer l'audio »."),
|
||||
("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."),
|
||||
("Wait", "En cours"),
|
||||
("Elevation Error", "Erreur d'augmentation des privilèges"),
|
||||
@ -451,37 +451,45 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Auto"),
|
||||
("Other Default Options", "Autres options par défaut"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
("relay_hint_tip", ""),
|
||||
("Reconnect", ""),
|
||||
("Codec", ""),
|
||||
("Resolution", ""),
|
||||
("No transfers in progress", ""),
|
||||
("Set one-time password length", ""),
|
||||
("idd_driver_tip", ""),
|
||||
("confirm_idd_driver_tip", ""),
|
||||
("RDP Settings", ""),
|
||||
("Sort by", ""),
|
||||
("New Connection", ""),
|
||||
("Restore", ""),
|
||||
("Minimize", ""),
|
||||
("Maximize", ""),
|
||||
("Your Device", ""),
|
||||
("empty_recent_tip", ""),
|
||||
("empty_favorite_tip", ""),
|
||||
("empty_lan_tip", ""),
|
||||
("empty_address_book_tip", ""),
|
||||
("eg: admin", ""),
|
||||
("Empty Username", ""),
|
||||
("Empty Password", ""),
|
||||
("Me", ""),
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("Voice call", "Appel voix"),
|
||||
("Text chat", "Conversation textuelfle"),
|
||||
("Stop voice call", "Stopper l'appel voix"),
|
||||
("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche pair."),
|
||||
("Reconnect", "Se reconnecter"),
|
||||
("Codec", "Codec"),
|
||||
("Resolution", "Résolution"),
|
||||
("No transfers in progress", "Pas de transfert en cours"),
|
||||
("Set one-time password length", "Définir la longueur du mot de passe à usage unique"),
|
||||
("idd_driver_tip", "Installez le pilote d'affichage virtuel pour être utilisé lorsque vous n'avez pas d'affichage physique."),
|
||||
("confirm_idd_driver_tip", "L'option d'installation du pilote d'affichage virtuel est cochée. Notez qu'un certificat de test sera installé pour faire confiance au pilote d'affichage virtuel. Ce certificat de test ne sera utilisé que pour faire confiance aux pilotes Rustdesk."),
|
||||
("RDP Settings", "Configuration RDP"),
|
||||
("Sort by", "Trier par"),
|
||||
("New Connection", "Nouvelle connexion"),
|
||||
("Restore", "Restaurer"),
|
||||
("Minimize", "Minimiser"),
|
||||
("Maximize", "Maximiser"),
|
||||
("Your Device", "Votre appareil"),
|
||||
("empty_recent_tip", "Oups, pas de sessions récentes!\nIl est temps d'en prévoir une nouvelle."),
|
||||
("empty_favorite_tip", "Vous n'avez pas encore de pairs favoris?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-le à vos favoris!"),
|
||||
("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore de pairs découverts."),
|
||||
("empty_address_book_tip", "Ouh là là! il semble qu'il n'y ait actuellement aucun pair répertorié dans votre carnet d'adresses."),
|
||||
("eg: admin", "ex: admin"),
|
||||
("Empty Username", "Nom d'utilisation non spécifié"),
|
||||
("Empty Password", "Mot de passe non spécifié"),
|
||||
("Me", "Moi"),
|
||||
("identical_file_tip", "Ce fichier est identique à celui du pair."),
|
||||
("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils."),
|
||||
("View Mode", "Mode vue"),
|
||||
("login_linux_tip", "Se connecter au compte Linux distant"),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Questo file è identico a quello del peer."),
|
||||
("show_monitors_tip", "Mostra schermi nella barra degli strumenti"),
|
||||
("View Mode", "Modalità di visualizzazione"),
|
||||
("enter_rustdesk_passwd_tip", "Inserisci la password di RustDesk."),
|
||||
("remember_rustdesk_passwd_tip", "Ricorda la passowrd di RustDesk."),
|
||||
("login_linux_tip", "Effettua l'accesso sul tuo account Linux"),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
495
src/lang/lt.rs
Normal file
495
src/lang/lt.rs
Normal file
@ -0,0 +1,495 @@
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "Būsena"),
|
||||
("Your Desktop", "Jūsų darbalaukis"),
|
||||
("desk_tip", "Jūsų darbalaukis pasiekiamas naudojant šį ID ir slaptažodį"),
|
||||
("Password", "Slaptažodis"),
|
||||
("Ready", "Pasiruošęs"),
|
||||
("Established", "Įsteigta"),
|
||||
("connecting_status", "Prisijungiama prie RustDesk tinklo..."),
|
||||
("Enable Service", "Įgalinti paslaugą"),
|
||||
("Start Service", "Pradėti paslaugą"),
|
||||
("Service is running", "Paslauga veikia"),
|
||||
("Service is not running", "Paslauga neveikia"),
|
||||
("not_ready_status", "Neprisijungęs. Patikrinkite ryšį."),
|
||||
("Control Remote Desktop", "Nuotolinio darbalaukio valdymas"),
|
||||
("Transfer File", "Perkelti failą"),
|
||||
("Connect", "Prisijungti"),
|
||||
("Recent Sessions", "Seansų istorija"),
|
||||
("Address Book", "Adresų knyga"),
|
||||
("Confirmation", "Patvirtinimas"),
|
||||
("TCP Tunneling", "TCP tuneliavimas"),
|
||||
("Remove", "Pašalinti"),
|
||||
("Refresh random password", "Atnaujinti atsitiktinį slaptažodį"),
|
||||
("Set your own password", "Nustatykite savo slaptažodį"),
|
||||
("Enable Keyboard/Mouse", "Įgalinti klaviatūrą/pelę"),
|
||||
("Enable Clipboard", "Įgalinti iškarpinę"),
|
||||
("Enable File Transfer", "Įgalinti failų perdavimą"),
|
||||
("Enable TCP Tunneling", "Įgalinti TCP tuneliavimą"),
|
||||
("IP Whitelisting", "IP baltasis sąrašas"),
|
||||
("ID/Relay Server", "ID / perdavimo serveris"),
|
||||
("Import Server Config", "Importuoti serverio konfigūraciją"),
|
||||
("Export Server Config", "Eksportuoti serverio konfigūraciją"),
|
||||
("Import server configuration successfully", "Sėkmingai importuoti serverio konfigūraciją"),
|
||||
("Export server configuration successfully", "Sėkmingai eksportuoti serverio konfigūraciją"),
|
||||
("Invalid server configuration", "Netinkama serverio konfigūracija"),
|
||||
("Clipboard is empty", "Iškarpinė tuščia"),
|
||||
("Stop service", "Sustabdyti paslaugą"),
|
||||
("Change ID", "Keisti ID"),
|
||||
("Your new ID", "Jūsų naujasis ID"),
|
||||
("length %min% to %max%", "ilgis %min% iki %max%"),
|
||||
("starts with a letter", "prasideda raide"),
|
||||
("allowed characters", "leistini simboliai"),
|
||||
("id_change_tip", "Leidžiami tik simboliai a–z, A–Z, 0–9 ir _ (pabraukimas). Pirmoji raidė turi būti a-z, A-Z. Ilgis nuo 6 iki 16."),
|
||||
("Website", "Interneto svetainė"),
|
||||
("About", "Apie"),
|
||||
("Slogan_tip", "Sukurta su siela šiame beprotiškame pasaulyje!"),
|
||||
("Privacy Statement", "Privatumo pareiškimas"),
|
||||
("Mute", "Nutildyti"),
|
||||
("Build Date", "Sukūrimo data"),
|
||||
("Version", "Versija"),
|
||||
("Home", "Namai"),
|
||||
("Audio Input", "Garso įvestis"),
|
||||
("Enhancements", "Patobulinimai"),
|
||||
("Hardware Codec", "Aparatinės įrangos paspartinimas"),
|
||||
("Adaptive Bitrate", "Adaptyvusis pralaidumas"),
|
||||
("ID Server", "ID serveris"),
|
||||
("Relay Server", "Perdavimo serveris"),
|
||||
("API Server", "API serveris"),
|
||||
("invalid_http", "Turi prasidėti http:// arba https://"),
|
||||
("Invalid IP", "Netinkamas IP"),
|
||||
("Invalid format", "Neteisingas formatas"),
|
||||
("server_not_support", "Serveris dar nepalaikomas"),
|
||||
("Not available", "Nepasiekiamas"),
|
||||
("Too frequent", "Per dažnai"),
|
||||
("Cancel", "Atšaukti"),
|
||||
("Skip", "Praleisti"),
|
||||
("Close", "Uždaryti"),
|
||||
("Retry", "Bandykite dar kartą"),
|
||||
("OK", "GERAI"),
|
||||
("Password Required", "Reikalingas slaptažodis"),
|
||||
("Please enter your password", "Prašome įvesti savo slaptažodį"),
|
||||
("Remember password", "Prisiminti slaptažodį"),
|
||||
("Wrong Password", "Neteisingas slaptažodis"),
|
||||
("Do you want to enter again?", "Ar norite įeiti dar kartą?"),
|
||||
("Connection Error", "Ryšio klaida"),
|
||||
("Error", "Klaida"),
|
||||
("Reset by the peer", "Atmetė nuotolinis kompiuteris"),
|
||||
("Connecting...", "Jungiamasi..."),
|
||||
("Connection in progress. Please wait.", "Jungiamasi. Palaukite."),
|
||||
("Please try 1 minute later", "Prašome pabandyti po 1 minutės"),
|
||||
("Login Error", "Prisijungimo klaida"),
|
||||
("Successful", "Sėkmingai"),
|
||||
("Connected, waiting for image...", "Prisijungta, laukiama vaizdo..."),
|
||||
("Name", "Vardas"),
|
||||
("Type", "Tipas"),
|
||||
("Modified", "Pakeista"),
|
||||
("Size", "Dydis"),
|
||||
("Show Hidden Files", "Rodyti paslėptus failus"),
|
||||
("Receive", "Gauti"),
|
||||
("Send", "Siųsti"),
|
||||
("Refresh File", "Atnaujinti failą"),
|
||||
("Local", "Vietinis"),
|
||||
("Remote", "Nuotolinis"),
|
||||
("Remote Computer", "Nuotolinis kompiuteris"),
|
||||
("Local Computer", "Šis kompiuteris"),
|
||||
("Confirm Delete", "Patvirtinti ištrynimą"),
|
||||
("Delete", "Ištrinti"),
|
||||
("Properties", "Ypatybės"),
|
||||
("Multi Select", "Keli pasirinkimas"),
|
||||
("Select All", "Pasirinkti viską"),
|
||||
("Unselect All", "Atšaukti visų pasirinkimą"),
|
||||
("Empty Directory", "Tuščias katalogas"),
|
||||
("Not an empty directory", "Ne tuščias katalogas"),
|
||||
("Are you sure you want to delete this file?", "Ar tikrai norite ištrinti šį failą?"),
|
||||
("Are you sure you want to delete this empty directory?", "Ar tikrai norite ištrinti šį tuščią katalogą?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Ar tikrai norite ištrinti šio katalogo failą?"),
|
||||
("Do this for all conflicts", "Taikyti visiems konfliktams"),
|
||||
("This is irreversible!", "Tai negrįžtama!"),
|
||||
("Deleting", "Ištrinama"),
|
||||
("files", "failai"),
|
||||
("Waiting", "Laukiu"),
|
||||
("Finished", "Baigta"),
|
||||
("Speed", "Greitis"),
|
||||
("Custom Image Quality", "Tinkinta vaizdo kokybė"),
|
||||
("Privacy mode", "Privatumo režimas"),
|
||||
("Block user input", "Blokuoti naudotojo įvestį"),
|
||||
("Unblock user input", "Atblokuoti naudotojo įvestį"),
|
||||
("Adjust Window", "Koreguoti langą"),
|
||||
("Original", "Originalas"),
|
||||
("Shrink", "Susitraukti"),
|
||||
("Stretch", "Ištempti"),
|
||||
("Scrollbar", "Slinkties juosta"),
|
||||
("ScrollAuto", "Automatinis slinkimas"),
|
||||
("Good image quality", "Gera vaizdo kokybė"),
|
||||
("Balanced", "Subalansuotas"),
|
||||
("Optimize reaction time", "Optimizuoti reakcijos laiką"),
|
||||
("Custom", "Tinkintas"),
|
||||
("Show remote cursor", "Rodyti nuotolinį žymeklį"),
|
||||
("Show quality monitor", "Rodyti kokybės monitorių"),
|
||||
("Disable clipboard", "Išjungti mainų sritį"),
|
||||
("Lock after session end", "Užrakinti pasibaigus seansui"),
|
||||
("Insert", "Įdėti"),
|
||||
("Insert Lock", "Įterpti užraktą"),
|
||||
("Refresh", "Atnaujinti"),
|
||||
("ID does not exist", "ID neegzistuoja"),
|
||||
("Failed to connect to rendezvous server", "Nepavyko prisijungti prie susitikimo serverio"),
|
||||
("Please try later", "Prašome pabandyti vėliau"),
|
||||
("Remote desktop is offline", "Nuotolinis darbalaukis neprisijungęs"),
|
||||
("Key mismatch", "Raktų neatitikimas"),
|
||||
("Timeout", "Laikas baigėsi"),
|
||||
("Failed to connect to relay server", "Nepavyko prisijungti prie perdavimo serverio"),
|
||||
("Failed to connect via rendezvous server", "Nepavyko prisijungti per susitikimo serverį"),
|
||||
("Failed to connect via relay server", "Nepavyko prisijungti per perdavimo serverį"),
|
||||
("Failed to make direct connection to remote desktop", "Nepavyko tiesiogiai prisijungti prie nuotolinio darbalaukio"),
|
||||
("Set Password", "Nustatyti slaptažodį"),
|
||||
("OS Password", "OS slaptažodis"),
|
||||
("install_tip", "Kai kuriais atvejais UAC gali priversti RustDesk netinkamai veikti nuotoliniame pagrindiniame kompiuteryje. Norėdami apeiti UAC, spustelėkite toliau esantį mygtuką, kad įdiegtumėte RustDesk į savo kompiuterį."),
|
||||
("Click to upgrade", "Spustelėkite, jei norite atnaujinti"),
|
||||
("Click to download", "Spustelėkite norėdami atsisiųsti"),
|
||||
("Click to update", "Spustelėkite norėdami atnaujinti"),
|
||||
("Configure", "Konfigūruoti"),
|
||||
("config_acc", "Norėdami nuotoliniu būdu valdyti darbalaukį, turite suteikti RustDesk \"prieigos\" leidimus"),
|
||||
("config_screen", "Norėdami nuotoliniu būdu pasiekti darbalaukį, turite suteikti RustDesk leidimus \"ekrano kopija\""),
|
||||
("Installing ...", "Diegiama ..."),
|
||||
("Install", "Diegti"),
|
||||
("Installation", "Įdiegimas"),
|
||||
("Installation Path", "Įdiegimo kelias"),
|
||||
("Create start menu shortcuts", "Sukurti pradžios meniu sparčiuosius klavišus"),
|
||||
("Create desktop icon", "Sukurti darbalaukio piktogramą"),
|
||||
("agreement_tip", "Pradėdami diegimą sutinkate su licencijos sutarties sąlygomis"),
|
||||
("Accept and Install", "Priimti ir įdiegti"),
|
||||
("End-user license agreement", "Galutinio vartotojo licencijos sutartis"),
|
||||
("Generating ...", "Generuojamas..."),
|
||||
("Your installation is lower version.", "Jūsų įdiegta versija senesnė."),
|
||||
("not_close_tcp_tip", "Naudodami tunelį neuždarykite šio lango"),
|
||||
("Listening ...", "Laukimas..."),
|
||||
("Remote Host", "Nuotolinis pagrindinis kompiuteris"),
|
||||
("Remote Port", "Nuotolinis prievadas"),
|
||||
("Action", "Veiksmas"),
|
||||
("Add", "Papildyti"),
|
||||
("Local Port", "Vietinis prievadas"),
|
||||
("Local Address", "Vietinis adresas"),
|
||||
("Change Local Port", "Keisti vietinį prievadą"),
|
||||
("setup_server_tip", "Kad ryšys būtų greitesnis, nustatykite savo serverį"),
|
||||
("Too short, at least 6 characters.", "Per trumpas, mažiausiai 6 simboliai."),
|
||||
("The confirmation is not identical.", "Patvirtinimas nėra tapatus."),
|
||||
("Permissions", "Leidimai"),
|
||||
("Accept", "Priimti"),
|
||||
("Dismiss", "Atmesti"),
|
||||
("Disconnect", "Atjungti"),
|
||||
("Allow using keyboard and mouse", "Leisti naudojant klaviatūrą ir pelę"),
|
||||
("Allow using clipboard", "Leisti naudoti mainų sritį"),
|
||||
("Allow hearing sound", "Leisti girdėti garsą"),
|
||||
("Allow file copy and paste", "Leisti failą kopijuoti ir įklijuoti"),
|
||||
("Connected", "Prisijungta"),
|
||||
("Direct and encrypted connection", "Tiesioginis ir šifruotas ryšys"),
|
||||
("Relayed and encrypted connection", "Perduotas ir šifruotas ryšys"),
|
||||
("Direct and unencrypted connection", "Tiesioginis ir nešifruotas ryšys"),
|
||||
("Relayed and unencrypted connection", "Perduotas ir nešifruotas ryšys"),
|
||||
("Enter Remote ID", "Įveskite nuotolinio ID"),
|
||||
("Enter your password", "Įveskite savo slaptažodį"),
|
||||
("Logging in...", "Prisijungiama..."),
|
||||
("Enable RDP session sharing", "Įgalinti RDP seansų bendrinimą"),
|
||||
("Auto Login", "Automatinis prisijungimas"),
|
||||
("Enable Direct IP Access", "Įgalinti tiesioginę IP prieigą"),
|
||||
("Rename", "Pervardyti"),
|
||||
("Space", "Erdvė"),
|
||||
("Create Desktop Shortcut", "Sukurti nuorodą darbalaukyje"),
|
||||
("Change Path", "Keisti kelią"),
|
||||
("Create Folder", "Sukurti aplanką"),
|
||||
("Please enter the folder name", "Įveskite aplanko pavadinimą"),
|
||||
("Fix it", "Pataisyk tai"),
|
||||
("Warning", "Įspėjimas"),
|
||||
("Login screen using Wayland is not supported", "Prisijungimo ekranas naudojant Wayland nepalaikomas"),
|
||||
("Reboot required", "Reikia paleisti iš naujo"),
|
||||
("Unsupported display server", "Nepalaikomas rodymo serveris"),
|
||||
("x11 expected", "reikalingas x11"),
|
||||
("Port", "Prievadas"),
|
||||
("Settings", "Nustatymai"),
|
||||
("Username", "Vartotojo vardas"),
|
||||
("Invalid port", "Netinkamas prievadas"),
|
||||
("Closed manually by the peer", "Partneris atmetė prašymą prisijungti"),
|
||||
("Enable remote configuration modification", "Įgalinti nuotolinį konfigūracijos modifikavimą"),
|
||||
("Run without install", "Vykdyti be diegimo"),
|
||||
("Connect via relay", "Prisijungti per relę"),
|
||||
("Always connect via relay", "Visada prisijunkite per relę"),
|
||||
("whitelist_tip", "Mane gali pasiekti tik baltajame sąraše esantys IP adresai"),
|
||||
("Login", "Prisijungti"),
|
||||
("Verify", "Patvirtinti"),
|
||||
("Remember me", "Prisimink mane"),
|
||||
("Trust this device", "Pasitikėk šiuo įrenginiu"),
|
||||
("Verification code", "Patvirtinimo kodas"),
|
||||
("verification_tip", "Aptiktas naujas įrenginys ir registruotu el. pašto adresu išsiųstas patvirtinimo kodas. Įveskite jį norėdami tęsti prisijungimą."),
|
||||
("Logout", "Atsijungti"),
|
||||
("Tags", "Žymos"),
|
||||
("Search ID", "Paieškos ID"),
|
||||
("whitelist_sep", "Atskirti kableliu, kabliataškiu, tarpu arba nauja eilute"),
|
||||
("Add ID", "Pridėti ID"),
|
||||
("Add Tag", "Pridėti žymą"),
|
||||
("Unselect all tags", "Atšaukti visų žymų pasirinkimą"),
|
||||
("Network error", "Tinklo klaida"),
|
||||
("Username missed", "Prarastas vartotojo vardas"),
|
||||
("Password missed", "Slaptažodis praleistas"),
|
||||
("Wrong credentials", "Klaidingi kredencialai"),
|
||||
("Edit Tag", "Redaguoti žymą"),
|
||||
("Unremember Password", "Nebeprisiminti slaptažodžio"),
|
||||
("Favorites", "Parankiniai"),
|
||||
("Add to Favorites", "Įtraukti į parankinius"),
|
||||
("Remove from Favorites", "Pašalinti iš parankinių"),
|
||||
("Empty", "Tuščia"),
|
||||
("Invalid folder name", "Neteisingas aplanko pavadinimas"),
|
||||
("Socks5 Proxy", "Socks5 Proxy"),
|
||||
("Hostname", "Pagrindinio kompiuterio pavadinimas"),
|
||||
("Discovered", "Aptikta tinkle"),
|
||||
("install_daemon_tip", "Norėdami, kad RustDesk startuotų automatiškai, turite ją įdiegti"),
|
||||
("Remote ID", "Nuotolinis ID"),
|
||||
("Paste", "Įklijuoti"),
|
||||
("Paste here?", "Įklijuoti čia?"),
|
||||
("Are you sure to close the connection?", "Ar tikrai norite atsijungti?"),
|
||||
("Download new version", "Atsisiųsti naują versiją"),
|
||||
("Touch mode", "Palietimo režimas"),
|
||||
("Mouse mode", "Pelės režimas"),
|
||||
("One-Finger Tap", "Palietimas vienu pirštu"),
|
||||
("Left Mouse", "Kairysis pelės kl."),
|
||||
("One-Long Tap", "Vienas palietimas"),
|
||||
("Two-Finger Tap", "Palietimas dviem pirštais"),
|
||||
("Right Mouse", "Dešinysis pelės kl."),
|
||||
("One-Finger Move", "Vieno piršto judesys"),
|
||||
("Double Tap & Move", "Dukart palieskite ir perkelkite"),
|
||||
("Mouse Drag", "Pelės vilkimas"),
|
||||
("Three-Finger vertically", "Trys pirštai vertikaliai"),
|
||||
("Mouse Wheel", "Pelės ratukas"),
|
||||
("Two-Finger Move", "Dviejų pirštų judesys"),
|
||||
("Canvas Move", "Drobės perkėlimas"),
|
||||
("Pinch to Zoom", "Suimkite, kad padidintumėte"),
|
||||
("Canvas Zoom", "Drobės mastelis"),
|
||||
("Reset canvas", "Atstatyti drobę"),
|
||||
("No permission of file transfer", "Nėra leidimo perkelti failus"),
|
||||
("Note", "Pastaba"),
|
||||
("Connection", "Ryšys"),
|
||||
("Share Screen", "Bendrinti ekraną"),
|
||||
("Chat", "Pokalbis"),
|
||||
("Total", "Iš viso"),
|
||||
("items", "elementai"),
|
||||
("Selected", "Pasirinkta"),
|
||||
("Screen Capture", "Ekrano nuotrauka"),
|
||||
("Input Control", "Įvesties valdymas"),
|
||||
("Audio Capture", "Garso fiksavimas"),
|
||||
("File Connection", "Failo ryšys"),
|
||||
("Screen Connection", "Ekrano jungtis"),
|
||||
("Do you accept?", "Ar sutinki?"),
|
||||
("Open System Setting", "Atviros sistemos nustatymas"),
|
||||
("How to get Android input permission?", "Kaip gauti Android įvesties leidimą?"),
|
||||
("android_input_permission_tip1", "Kad nuotolinis įrenginys galėtų valdyti Android įrenginį pele arba liesti, turite leisti RustDesk naudoti \"Prieinamumo\" paslaugą."),
|
||||
("android_input_permission_tip2", "Eikite į kitą sistemos nustatymų puslapį, suraskite \"Įdiegtos paslaugos\" ir įgalinkite \"RustDesk įvestis\" paslaugą."),
|
||||
("android_new_connection_tip", "Gauta nauja užklausa tvarkyti dabartinį įrenginį."),
|
||||
("android_service_will_start_tip", "Įgalinus ekrano fiksavimo paslaugą, kiti įrenginiai gali pateikti užklausą prisijungti prie to įrenginio."),
|
||||
("android_stop_service_tip", "Uždarius paslaugą automatiškai bus uždaryti visi užmegzti ryšiai."),
|
||||
("android_version_audio_tip", "Dabartinė Android versija nepalaiko garso įrašymo, atnaujinkite į Android 10 ar naujesnę versiją."),
|
||||
("android_start_service_tip", "Spustelėkite [Paleisti paslaugą] arba įjunkite [Fiksuoti ekraną], kad paleistumėte ekrano bendrinimo paslaugą."),
|
||||
("android_permission_may_not_change_tip", "Užmegztų ryšių leidimų keisti negalima, reikia prisijungti iš naujo."),
|
||||
("Account", "Paskyra"),
|
||||
("Overwrite", "Perrašyti"),
|
||||
("This file exists, skip or overwrite this file?", "Šis failas egzistuoja, praleisti arba perrašyti šį failą?"),
|
||||
("Quit", "Išeiti"),
|
||||
("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/"),
|
||||
("Help", "Pagalba"),
|
||||
("Failed", "Nepavyko"),
|
||||
("Succeeded", "Pavyko"),
|
||||
("Someone turns on privacy mode, exit", "Kažkas įjungė privatumo režimą, išeiti"),
|
||||
("Unsupported", "Nepalaikomas"),
|
||||
("Peer denied", "Atšaukė"),
|
||||
("Please install plugins", "Įdiekite papildinius"),
|
||||
("Peer exit", "Nuotolinis mazgas neveikia"),
|
||||
("Failed to turn off", "Nepavyko išjungti"),
|
||||
("Turned off", "Išjungti"),
|
||||
("In privacy mode", "Privatumo režimas"),
|
||||
("Out privacy mode", "Išėjimas iš privatumo režimo"),
|
||||
("Language", "Kalba"),
|
||||
("Keep RustDesk background service", "Palikti RustDesk fonine paslauga"),
|
||||
("Ignore Battery Optimizations", "Ignoruoti akumuliatoriaus optimizavimą"),
|
||||
("android_open_battery_optimizations_tip", "Eikite į kitą nustatymų puslapį"),
|
||||
("Start on Boot", "Pradėti paleidžiant"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "Paleiskite ekrano bendrinimo paslaugą įkrovos metu, reikia specialių leidimų"),
|
||||
("Connection not allowed", "Ryšys neleidžiamas"),
|
||||
("Legacy mode", "Senasis režimas"),
|
||||
("Map mode", "Žemėlapio režimas"),
|
||||
("Translate mode", "Vertimo režimas"),
|
||||
("Use permanent password", "Naudoti nuolatinį slaptažodį"),
|
||||
("Use both passwords", "Naudoti abu slaptažodžius"),
|
||||
("Set permanent password", "Nustatyti nuolatinį slaptažodį"),
|
||||
("Enable Remote Restart", "Įgalinti nuotolinį paleidimą iš naujo"),
|
||||
("Allow remote restart", "Leisti nuotolinį paleidimą iš naujo"),
|
||||
("Restart Remote Device", "Paleisti nuotolinį įrenginį iš naujo"),
|
||||
("Are you sure you want to restart", "Ar tikrai norite paleisti iš naujo"),
|
||||
("Restarting Remote Device", "Nuotolinio įrenginio paleidimas iš naujo"),
|
||||
("remote_restarting_tip", "Nuotolinis įrenginys paleidžiamas iš naujo. Uždarykite šį pranešimą ir po kurio laiko vėl prisijunkite naudodami nuolatinį slaptažodį."),
|
||||
("Copied", "Nukopijuota"),
|
||||
("Exit Fullscreen", "Išeiti iš pilno ekrano"),
|
||||
("Fullscreen", "Per visą ekraną"),
|
||||
("Mobile Actions", "Veiksmai mobiliesiems"),
|
||||
("Select Monitor", "Pasirinkite monitorių"),
|
||||
("Control Actions", "Valdymo veiksmai"),
|
||||
("Display Settings", "Ekrano nustatymai"),
|
||||
("Ratio", "Santykis"),
|
||||
("Image Quality", "Vaizdo kokybė"),
|
||||
("Scroll Style", "Slinkimo stilius"),
|
||||
("Show Menubar", "Rodyti meniu juostą"),
|
||||
("Hide Menubar", "Slėpti meniu juostą"),
|
||||
("Direct Connection", "Tiesioginis ryšys"),
|
||||
("Relay Connection", "Tarpinė jungtis"),
|
||||
("Secure Connection", "Saugus ryšys"),
|
||||
("Insecure Connection", "Nesaugus ryšys"),
|
||||
("Scale original", "Pakeisti originalų mastelį"),
|
||||
("Scale adaptive", "Pritaikomas mastelis"),
|
||||
("General", "Bendra"),
|
||||
("Security", "Sauga"),
|
||||
("Theme", "Tema"),
|
||||
("Dark Theme", "Tamsioji tema"),
|
||||
("Light Theme", "Šviesi tema"),
|
||||
("Dark", "Tamsi"),
|
||||
("Light", "Šviesi"),
|
||||
("Follow System", "Kaip sistemos"),
|
||||
("Enable hardware codec", "Įgalinti"),
|
||||
("Unlock Security Settings", "Atrakinti saugos nustatymus"),
|
||||
("Enable Audio", "Įgalinti garsą"),
|
||||
("Unlock Network Settings", "Atrakinti tinklo nustatymus"),
|
||||
("Server", "Serveris"),
|
||||
("Direct IP Access", "Tiesioginė IP prieiga"),
|
||||
("Proxy", "Tarpinis serveris"),
|
||||
("Apply", "Taikyti"),
|
||||
("Disconnect all devices?", "Atjungti visus įrenginius?"),
|
||||
("Clear", "Išvalyti"),
|
||||
("Audio Input Device", "Garso įvestis"),
|
||||
("Deny remote access", "Uždrausti nuotolinę prieigą"),
|
||||
("Use IP Whitelisting", "Naudoti patikimą IP sąrašą"),
|
||||
("Network", "Tinklas"),
|
||||
("Enable RDP", "Įgalinti RDP"),
|
||||
("Pin menubar", "Prisegti meniu juostą"),
|
||||
("Unpin menubar", "Atsegti meniu juostą"),
|
||||
("Recording", "Įrašymas"),
|
||||
("Directory", "Katalogas"),
|
||||
("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"),
|
||||
("Change", "Keisti"),
|
||||
("Start session recording", "Pradėti seanso įrašymą"),
|
||||
("Stop session recording", "Sustabdyti seanso įrašymą"),
|
||||
("Enable Recording Session", "Įgalinti įrašymo seansą"),
|
||||
("Allow recording session", "Leisti įrašymo sesiją"),
|
||||
("Enable LAN Discovery", "Įgalinti LAN aptikimą"),
|
||||
("Deny LAN Discovery", "Neleisti LAN aptikimo"),
|
||||
("Write a message", "Rašyti žinutę"),
|
||||
("Prompt", "Užuomina"),
|
||||
("Please wait for confirmation of UAC...", "Palaukite UAC patvirtinimo..."),
|
||||
("elevated_foreground_window_tip", "Dabartinis nuotolinio darbalaukio langas reikalauja didesnių privilegijų, todėl laikinai neįmanoma naudoti pelės ir klaviatūros. Galite paprašyti nuotolinio vartotojo sumažinti dabartinį langą arba spustelėti aukščio mygtuką ryšio valdymo lange. Norint išvengti šios problemos ateityje, rekomenduojama programinę įrangą įdiegti nuotoliniame įrenginyje."),
|
||||
("Disconnected", "Atjungtas"),
|
||||
("Other", "Kita"),
|
||||
("Confirm before closing multiple tabs", "Patvirtinti prieš uždarant kelis skirtukus"),
|
||||
("Keyboard Settings", "Klaviatūros nustatymai"),
|
||||
("Full Access", "Pilna prieiga"),
|
||||
("Screen Share", "Ekrano bendrinimas"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."),
|
||||
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."),
|
||||
("JumpLink", "Peržiūra"),
|
||||
("Please Select the screen to be shared(Operate on the peer side).", "Prašome pasirinkti ekraną, kurį norite bendrinti (veikiantį kitoje pusėje)."),
|
||||
("Show RustDesk", "Rodyti RustDesk"),
|
||||
("This PC", "Šis kompiuteris"),
|
||||
("or", "arba"),
|
||||
("Continue with", "Tęsti su"),
|
||||
("Elevate", "Pakelti"),
|
||||
("Zoom cursor", "Mastelio keitimo žymeklis"),
|
||||
("Accept sessions via password", "Priimti seansus naudojant slaptažodį"),
|
||||
("Accept sessions via click", "Priimti seansus spustelėjus"),
|
||||
("Accept sessions via both", "Priimti seansus abiem variantais"),
|
||||
("Please wait for the remote side to accept your session request...", "Palaukite, kol nuotolinė pusė priims jūsų seanso užklausą..."),
|
||||
("One-time Password", "Vienkartinis slaptažodis"),
|
||||
("Use one-time password", "Naudoti vienkartinį slaptažodį"),
|
||||
("One-time password length", "Vienkartinio slaptažodžio ilgis"),
|
||||
("Request access to your device", "Prašo leidimo valdyti jūsų įrenginį"),
|
||||
("Hide connection management window", "Slėpti ryšio valdymo langą"),
|
||||
("hide_cm_tip", "Leisti paslėpti didžiąją ir mažąją raidę, jei priimamos slaptažodžio sesijos arba naudojamas nuolatinis slaptažodis"),
|
||||
("wayland_experiment_tip", "Wayland palaikymas yra eksperimentinis, naudokite X11, jei jums reikalingas automatinis prisijungimas."),
|
||||
("Right click to select tabs", "Dešiniuoju pelės mygtuku spustelėkite, kad pasirinktumėte skirtukus"),
|
||||
("Skipped", "Praleisti"),
|
||||
("Add to Address Book", "Pridėti prie adresų knygos"),
|
||||
("Group", "Grupė"),
|
||||
("Search", "Paieška"),
|
||||
("Closed manually by web console", "Uždaryta rankiniu būdu naudojant žiniatinklio konsolę"),
|
||||
("Local keyboard type", "Vietinės klaviatūros tipas"),
|
||||
("Select local keyboard type", "Pasirinkite vietinės klaviatūros tipą"),
|
||||
("software_render_tip", "Jei turite Nvidia vaizdo plokštę ir nuotolinis langas iškart užsidaro prisijungus, gali padėti „Nouveau“ tvarkyklės įdiegimas ir programinės įrangos atvaizdavimo pasirinkimas. Būtina paleisti iš naujo."),
|
||||
("Always use software rendering", "Visada naudoti programinį spartintuvą"),
|
||||
("config_input", "Norėdami valdyti nuotolinį darbalaukį naudodami klaviatūrą, turite suteikti RustDesk leidimus \"Įvesties monitoringas\"."),
|
||||
("config_microphone", "Norėdami kalbėtis su nuotoline puse, turite suteikti RustDesk leidimą \"Įrašyti garsą\"."),
|
||||
("request_elevation_tip", "Taip pat galite prašyti tesių suteikimo, jeigu kas nors yra nuotolinėje pusėje."),
|
||||
("Wait", "Laukti"),
|
||||
("Elevation Error", "Teisių suteikimo klaida"),
|
||||
("Ask the remote user for authentication", "Klauskite nuotolinio vartotojo autentifikavimo"),
|
||||
("Choose this if the remote account is administrator", "Pasirinkite tai, jei nuotolinė paskyra yra administratorius"),
|
||||
("Transmit the username and password of administrator", "Persiųsti administratoriaus vartotojo vardą ir slaptažodį"),
|
||||
("still_click_uac_tip", "Vis tiek reikia, kad nuotolinis vartotojas paleidžiant RustDesk UAC lange paspaustų \"OK\"."),
|
||||
("Request Elevation", "Prašyti teisių"),
|
||||
("wait_accept_uac_tip", "Palaukite, kol nuotolinis vartotojas patvirtins UAC užklausą."),
|
||||
("Elevate successfully", "Teisės suteiktos"),
|
||||
("uppercase", "didžiosios raidės"),
|
||||
("lowercase", "mažosios raidės"),
|
||||
("digit", "skaitmuo"),
|
||||
("special character", "specialusis simbolis"),
|
||||
("length>=8", "ilgis>=8"),
|
||||
("Weak", "Silpnas"),
|
||||
("Medium", "Vidutinis"),
|
||||
("Strong", "Stiprus"),
|
||||
("Switch Sides", "Perjungti puses"),
|
||||
("Please confirm if you want to share your desktop?", "Prašome patvirtinti, jeigu norite bendrinti darbalaukį?"),
|
||||
("Display", "Ekranas"),
|
||||
("Default View Style", "Numatytasis peržiūros stilius"),
|
||||
("Default Scroll Style", "Numatytasis slinkties stilius"),
|
||||
("Default Image Quality", "Numatytoji vaizdo kokybė"),
|
||||
("Default Codec", "Numatytasis kodekas"),
|
||||
("Bitrate", "Sparta"),
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Automatinis"),
|
||||
("Other Default Options", "Kitos numatytosios parinktys"),
|
||||
("Voice call", "Balso skambutis"),
|
||||
("Text chat", "Tekstinis pokalbis"),
|
||||
("Stop voice call", "Sustabdyti balso skambutį"),
|
||||
("relay_hint_tip", "Tiesioginis ryšys gali būti neįmanomas. Tokiu atveju galite pabandyti prisijungti per perdavimo serverį. \nArba, jei norite iš karto naudoti perdavimo serverį, prie ID galite pridėti priesagą \"/r\" arba nuotolinio pagrindinio kompiuterio nustatymuose įgalinti \"Visada prisijungti per relę\"."),
|
||||
("Reconnect", "Prisijungti iš naujo"),
|
||||
("Codec", "Kodekas"),
|
||||
("Resolution", "Rezoliucija"),
|
||||
("No transfers in progress", "Nevyksta jokių perdavimų"),
|
||||
("Set one-time password length", "Nustatyti vienkartinio slaptažodžio ilgį"),
|
||||
("idd_driver_tip", "Įdiekite virtualaus ekrano tvarkyklę (naudojama, kai nėra fizinių ekranų)"),
|
||||
("confirm_idd_driver_tip", "Įjungta virtualaus ekrano tvarkyklės diegimo funkcija. Atminkite, kad bus įdiegtas bandomasis sertifikatas, kad būtų galima pasitikėti tvarkykle. Šis sertifikatas bus naudojamas tik pasitikėjimui Rustdesk tvarkyklėmis patikrinti."),
|
||||
("RDP Settings", "RDP nustatymai"),
|
||||
("Sort by", "Rūšiuoti pagal"),
|
||||
("New Connection", "Naujas ryšys"),
|
||||
("Restore", "Atkurti"),
|
||||
("Minimize", "Sumažinti"),
|
||||
("Maximize", "Padidinti"),
|
||||
("Your Device", "Jūsų įrenginys"),
|
||||
("empty_recent_tip", "Nėra paskutinių seansų!\nLaikas suplanuoti naują."),
|
||||
("empty_favorite_tip", "Dar neturite parankinių nuotolinių seansų."),
|
||||
("empty_lan_tip", "Nuotolinių mazgų nerasta."),
|
||||
("empty_address_book_tip", "Adresų knygelėje nėra nuotolinių kompiuterių."),
|
||||
("eg: admin", "pvz.: administratorius"),
|
||||
("Empty Username", "Tuščias naudotojo vardas"),
|
||||
("Empty Password", "Tuščias slaptažodis"),
|
||||
("Me", "Aš"),
|
||||
("identical_file_tip", "Failas yra identiškas nuotoliniame kompiuteryje esančiam failui."),
|
||||
("show_monitors_tip", "Rodyti monitorius įrankių juostoje"),
|
||||
("View Mode", "Peržiūros režimas"),
|
||||
("login_linux_tip", "Norėdami įjungti X darbalaukio seansą, turite būti prisijungę prie nuotolinės Linux paskyros."),
|
||||
("verify_rustdesk_password_tip", "Įveskite kliento RustDesk slaptažodį"),
|
||||
("remember_account_tip", "Prisiminti šią paskyrą"),
|
||||
("os_account_desk_tip", "Ši paskyra naudojama norint prisijungti prie nuotolinės OS ir įgalinti darbalaukio seansą režimu headless"),
|
||||
("OS Account", "OS paskyra"),
|
||||
("another_user_login_title_tip", "Kitas vartotojas jau yra prisijungęs"),
|
||||
("another_user_login_text_tip", "Atjungti"),
|
||||
("xorg_not_found_title_tip", "Xorg nerastas"),
|
||||
("xorg_not_found_text_tip", "Prašom įdiegti Xorg"),
|
||||
("no_desktop_title_tip", "Nėra pasiekiamų nuotolinių darbalaukių"),
|
||||
("no_desktop_text_tip", "Prašom įdiegti GNOME Desktop"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Dit bestand is identiek aan het bestand van het externe station."),
|
||||
("show_monitors_tip", "Monitoren weergeven in de werkbalk"),
|
||||
("View Mode", "Weergave Mode"),
|
||||
("enter_rustdesk_passwd_tip", "Geef het RustDesk-wachtwoord op."),
|
||||
("remember_rustdesk_passwd_tip", "RustDesk Wachtwoord onthouden."),
|
||||
("login_linux_tip", "Je moet inloggen op een Linux Account op afstand om een X desktop sessie te openen."),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Ten plik jest identyczny jak plik na drugim komputerze."),
|
||||
("show_monitors_tip", "Pokaż monitory w zasobniku"),
|
||||
("View Mode", "Tryb widoku"),
|
||||
("enter_rustdesk_passwd_tip", "Podaj hasło RustDesk."),
|
||||
("remember_rustdesk_passwd_tip", "Zapamiętaj hasło RustDesk."),
|
||||
("login_linux_tip", "Zaloguj do zdalnego konta Linux"),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", "Файл идентичен файлу на удалённом узле."),
|
||||
("show_monitors_tip", "Показывать мониторы на панели инструментов"),
|
||||
("View Mode", "Режим просмотра"),
|
||||
("enter_rustdesk_passwd_tip", "Введите пароль RustDesk"),
|
||||
("remember_rustdesk_passwd_tip", "Запомнить пароль RustDesk"),
|
||||
("login_linux_tip", "Вход в удалённый аккаунт Linux"),
|
||||
("login_linux_tooltip_tip", "Чтобы включить сеанс рабочего стола X, необходимо войти в удалённый аккаунт Linux."),
|
||||
("login_linux_tip", "Чтобы включить сеанс рабочего стола X, необходимо войти в удалённый аккаунт Linux."),
|
||||
("verify_rustdesk_password_tip", "Подтвердить пароль RustDesk"),
|
||||
("remember_account_tip", "Запомнить этот аккаунт"),
|
||||
("os_account_desk_tip", "Эта учетная запись используется для входа в удаленную ОС и включения сеанса рабочего стола в режиме headless"),
|
||||
("OS Account", "Аккаунт ОС"),
|
||||
("another_user_login_title_tip", "Другой пользователь уже вошел в систему"),
|
||||
("another_user_login_text_tip", "Отключить"),
|
||||
("xorg_not_found_title_tip", "Xorg не найден"),
|
||||
("xorg_not_found_text_tip", "Установите, пожалуйста, Xorg"),
|
||||
("no_desktop_title_tip", "Нет доступных рабочих столов"),
|
||||
("no_desktop_text_tip", "Установите, пожалуйста, GNOME Desktop"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("login_linux_tooltip_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
983
src/lang/tw.rs
983
src/lang/tw.rs
@ -1,488 +1,495 @@
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "狀態"),
|
||||
("Your Desktop", "您的桌面"),
|
||||
("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"),
|
||||
("Password", "密碼"),
|
||||
("Ready", "就緒"),
|
||||
("Established", "已建立"),
|
||||
("connecting_status", "正在連線到 RustDesk 網路 ..."),
|
||||
("Enable Service", "啟用服務"),
|
||||
("Start Service", "啟動服務"),
|
||||
("Service is running", "服務正在執行"),
|
||||
("Service is not running", "服務尚未執行"),
|
||||
("not_ready_status", "尚未就緒,請檢查您的網路連線。"),
|
||||
("Control Remote Desktop", "控制遠端桌面"),
|
||||
("Transfer File", "傳輸檔案"),
|
||||
("Connect", "連線"),
|
||||
("Recent Sessions", "近期的工作階段"),
|
||||
("Address Book", "通訊錄"),
|
||||
("Confirmation", "確認"),
|
||||
("TCP Tunneling", "TCP 通道"),
|
||||
("Remove", "移除"),
|
||||
("Refresh random password", "重新產生隨機密碼"),
|
||||
("Set your own password", "自行設定密碼"),
|
||||
("Enable Keyboard/Mouse", "啟用鍵盤和滑鼠"),
|
||||
("Enable Clipboard", "啟用剪貼簿"),
|
||||
("Enable File Transfer", "啟用檔案傳輸"),
|
||||
("Enable TCP Tunneling", "啟用 TCP 通道"),
|
||||
("IP Whitelisting", "IP 白名單"),
|
||||
("ID/Relay Server", "ID / 轉送伺服器"),
|
||||
("Import Server Config", "匯入伺服器設定"),
|
||||
("Export Server Config", "匯出伺服器設定"),
|
||||
("Import server configuration successfully", "匯入伺服器設定成功"),
|
||||
("Export server configuration successfully", "匯出伺服器設定成功"),
|
||||
("Invalid server configuration", "無效的伺服器設定"),
|
||||
("Clipboard is empty", "剪貼簿是空的"),
|
||||
("Stop service", "停止服務"),
|
||||
("Change ID", "更改 ID"),
|
||||
("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", "隱私權聲明"),
|
||||
("Mute", "靜音"),
|
||||
("Build Date", "構建日期"),
|
||||
("Version", "版本"),
|
||||
("Home", "首頁"),
|
||||
("Audio Input", "音訊輸入"),
|
||||
("Enhancements", "增強功能"),
|
||||
("Hardware Codec", "硬體編解碼器"),
|
||||
("Adaptive Bitrate", "自適應位元速率"),
|
||||
("ID Server", "ID 伺服器"),
|
||||
("Relay Server", "轉送伺服器"),
|
||||
("API Server", "API 伺服器"),
|
||||
("invalid_http", "開頭必須為 http:// 或 https://"),
|
||||
("Invalid IP", "IP 無效"),
|
||||
("Invalid format", "格式無效"),
|
||||
("server_not_support", "伺服器暫不支持"),
|
||||
("Not available", "無法使用"),
|
||||
("Too frequent", "修改過於頻繁,請稍後再試。"),
|
||||
("Cancel", "取消"),
|
||||
("Skip", "跳過"),
|
||||
("Close", "關閉"),
|
||||
("Retry", "重試"),
|
||||
("OK", "確定"),
|
||||
("Password Required", "需要密碼"),
|
||||
("Please enter your password", "請輸入您的密碼"),
|
||||
("Remember password", "記住密碼"),
|
||||
("Wrong Password", "密碼錯誤"),
|
||||
("Do you want to enter again?", "您要重新輸入嗎?"),
|
||||
("Connection Error", "連線錯誤"),
|
||||
("Error", "錯誤"),
|
||||
("Reset by the peer", "對方重設了連線"),
|
||||
("Connecting...", "正在連線 ..."),
|
||||
("Connection in progress. Please wait.", "正在連線,請稍候。"),
|
||||
("Please try 1 minute later", "請於 1 分鐘後再試"),
|
||||
("Login Error", "登入錯誤"),
|
||||
("Successful", "成功"),
|
||||
("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."),
|
||||
("Name", "名稱"),
|
||||
("Type", "類型"),
|
||||
("Modified", "修改時間"),
|
||||
("Size", "大小"),
|
||||
("Show Hidden Files", "顯示隱藏檔案"),
|
||||
("Receive", "接收"),
|
||||
("Send", "傳送"),
|
||||
("Refresh File", "重新整理檔案"),
|
||||
("Local", "本地"),
|
||||
("Remote", "遠端"),
|
||||
("Remote Computer", "遠端電腦"),
|
||||
("Local Computer", "本地電腦"),
|
||||
("Confirm Delete", "確認刪除"),
|
||||
("Delete", "刪除"),
|
||||
("Properties", "屬性"),
|
||||
("Multi Select", "多選"),
|
||||
("Select All", "全選"),
|
||||
("Unselect All", "取消全選"),
|
||||
("Empty Directory", "空資料夾"),
|
||||
("Not an empty directory", "不是一個空資料夾"),
|
||||
("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"),
|
||||
("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"),
|
||||
("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"),
|
||||
("Do this for all conflicts", "套用到其他衝突"),
|
||||
("This is irreversible!", "此操作不可逆!"),
|
||||
("Deleting", "正在刪除 ..."),
|
||||
("files", "檔案"),
|
||||
("Waiting", "正在等候 ..."),
|
||||
("Finished", "已完成"),
|
||||
("Speed", "速度"),
|
||||
("Custom Image Quality", "自訂畫面品質"),
|
||||
("Privacy mode", "隱私模式"),
|
||||
("Block user input", "封鎖使用者輸入"),
|
||||
("Unblock user input", "取消封鎖使用者輸入"),
|
||||
("Adjust Window", "調整視窗"),
|
||||
("Original", "原始"),
|
||||
("Shrink", "縮減"),
|
||||
("Stretch", "延展"),
|
||||
("Scrollbar", "滾動條"),
|
||||
("ScrollAuto", "自動滾動"),
|
||||
("Good image quality", "最佳化畫面品質"),
|
||||
("Balanced", "平衡"),
|
||||
("Optimize reaction time", "最佳化反應時間"),
|
||||
("Custom", "自訂"),
|
||||
("Show remote cursor", "顯示遠端游標"),
|
||||
("Show quality monitor", "顯示質量監測"),
|
||||
("Disable clipboard", "停用剪貼簿"),
|
||||
("Lock after session end", "工作階段結束後鎖定電腦"),
|
||||
("Insert", "插入"),
|
||||
("Insert Lock", "鎖定遠端電腦"),
|
||||
("Refresh", "重新載入"),
|
||||
("ID does not exist", "ID 不存在"),
|
||||
("Failed to connect to rendezvous server", "無法連線到 rendezvous 伺服器"),
|
||||
("Please try later", "請稍候再試"),
|
||||
("Remote desktop is offline", "遠端桌面已離線"),
|
||||
("Key mismatch", "金鑰不符"),
|
||||
("Timeout", "逾時"),
|
||||
("Failed to connect to relay server", "無法連線到轉送伺服器"),
|
||||
("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"),
|
||||
("Failed to connect via relay server", "無法透過轉送伺服器連線"),
|
||||
("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"),
|
||||
("Set Password", "設定密碼"),
|
||||
("OS Password", "作業系統密碼"),
|
||||
("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"),
|
||||
("Click to upgrade", "點擊以升級"),
|
||||
("Click to download", "點擊以下載"),
|
||||
("Click to update", "點擊以更新"),
|
||||
("Configure", "設定"),
|
||||
("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"),
|
||||
("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"),
|
||||
("Installing ...", "正在安裝 ..."),
|
||||
("Install", "安裝"),
|
||||
("Installation", "安裝"),
|
||||
("Installation Path", "安裝路徑"),
|
||||
("Create start menu shortcuts", "新增開始功能表捷徑"),
|
||||
("Create desktop icon", "新增桌面捷徑"),
|
||||
("agreement_tip", "開始安裝即表示接受許可協議"),
|
||||
("Accept and Install", "接受並安裝"),
|
||||
("End-user license agreement", "使用者授權合約"),
|
||||
("Generating ...", "正在產生 ..."),
|
||||
("Your installation is lower version.", "您安裝的版本過舊。"),
|
||||
("not_close_tcp_tip", "使用通道時請不要關閉此視窗"),
|
||||
("Listening ...", "正在等待通道連線 ..."),
|
||||
("Remote Host", "遠端主機"),
|
||||
("Remote Port", "遠端連線端口"),
|
||||
("Action", "操作"),
|
||||
("Add", "新增"),
|
||||
("Local Port", "本機連線端口"),
|
||||
("Local Address", "本機地址"),
|
||||
("Change Local Port", "修改本機連線端口"),
|
||||
("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"),
|
||||
("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"),
|
||||
("The confirmation is not identical.", "兩次輸入不相符"),
|
||||
("Permissions", "權限"),
|
||||
("Accept", "接受"),
|
||||
("Dismiss", "關閉"),
|
||||
("Disconnect", "中斷連線"),
|
||||
("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"),
|
||||
("Allow using clipboard", "允許使用剪貼簿"),
|
||||
("Allow hearing sound", "允許分享音訊"),
|
||||
("Allow file copy and paste", "允許檔案複製和貼上"),
|
||||
("Connected", "已連線"),
|
||||
("Direct and encrypted connection", "加密直接連線"),
|
||||
("Relayed and encrypted connection", "加密轉送連線"),
|
||||
("Direct and unencrypted connection", "未加密直接連線"),
|
||||
("Relayed and unencrypted connection", "未加密轉送連線"),
|
||||
("Enter Remote ID", "輸入遠端 ID"),
|
||||
("Enter your password", "輸入您的密碼"),
|
||||
("Logging in...", "正在登入 ..."),
|
||||
("Enable RDP session sharing", "啟用 RDP 工作階段共享"),
|
||||
("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"),
|
||||
("Enable Direct IP Access", "允許 IP 直接存取"),
|
||||
("Rename", "重新命名"),
|
||||
("Space", "空白"),
|
||||
("Create Desktop Shortcut", "新增桌面捷徑"),
|
||||
("Change Path", "更改路徑"),
|
||||
("Create Folder", "新增資料夾"),
|
||||
("Please enter the folder name", "請輸入資料夾名稱"),
|
||||
("Fix it", "修復"),
|
||||
("Warning", "警告"),
|
||||
("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"),
|
||||
("Reboot required", "需要重新啟動"),
|
||||
("Unsupported display server", "不支援顯示伺服器"),
|
||||
("x11 expected", "預期 x11"),
|
||||
("Port", "端口"),
|
||||
("Settings", "設定"),
|
||||
("Username", "使用者名稱"),
|
||||
("Invalid port", "連線端口無效"),
|
||||
("Closed manually by the peer", "遠端使用者關閉了工作階段"),
|
||||
("Enable remote configuration modification", "允許遠端使用者更改設定"),
|
||||
("Run without install", "跳過安裝直接執行"),
|
||||
("Connect via relay", "中繼連線"),
|
||||
("Always connect via relay", "一律透過轉送連線"),
|
||||
("whitelist_tip", "只有白名單中的 IP 可以存取"),
|
||||
("Login", "登入"),
|
||||
("Verify", "驗證"),
|
||||
("Remember me", "記住我"),
|
||||
("Trust this device", "信任此裝置"),
|
||||
("Verification code", "驗證碼"),
|
||||
("verification_tip", "檢測到新裝置登入,已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"),
|
||||
("Logout", "登出"),
|
||||
("Tags", "標籤"),
|
||||
("Search ID", "搜尋 ID"),
|
||||
("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"),
|
||||
("Add ID", "新增 ID"),
|
||||
("Add Tag", "新增標籤"),
|
||||
("Unselect all tags", "取消選取所有標籤"),
|
||||
("Network error", "網路錯誤"),
|
||||
("Username missed", "缺少使用者名稱"),
|
||||
("Password missed", "缺少密碼"),
|
||||
("Wrong credentials", "提供的登入資訊有誤"),
|
||||
("Edit Tag", "編輯標籤"),
|
||||
("Unremember Password", "忘掉密碼"),
|
||||
("Favorites", "我的最愛"),
|
||||
("Add to Favorites", "新增到我的最愛"),
|
||||
("Remove from Favorites", "從我的最愛中刪除"),
|
||||
("Empty", "空空如也"),
|
||||
("Invalid folder name", "資料夾名稱無效"),
|
||||
("Socks5 Proxy", "Socks5 代理"),
|
||||
("Hostname", "主機名稱"),
|
||||
("Discovered", "已探索"),
|
||||
("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"),
|
||||
("Remote ID", "遠端 ID"),
|
||||
("Paste", "貼上"),
|
||||
("Paste here?", "貼上到這裡?"),
|
||||
("Are you sure to close the connection?", "您確定要關閉連線嗎?"),
|
||||
("Download new version", "下載新版本"),
|
||||
("Touch mode", "觸控模式"),
|
||||
("Mouse mode", "滑鼠模式"),
|
||||
("One-Finger Tap", "單指輕觸"),
|
||||
("Left Mouse", "滑鼠左鍵"),
|
||||
("One-Long Tap", "單指長按"),
|
||||
("Two-Finger Tap", "雙指輕觸"),
|
||||
("Right Mouse", "滑鼠右鍵"),
|
||||
("One-Finger Move", "單指移動"),
|
||||
("Double Tap & Move", "雙擊並移動"),
|
||||
("Mouse Drag", "滑鼠選中拖動"),
|
||||
("Three-Finger vertically", "三指垂直滑動"),
|
||||
("Mouse Wheel", "滑鼠滾輪"),
|
||||
("Two-Finger Move", "雙指移動"),
|
||||
("Canvas Move", "移動畫布"),
|
||||
("Pinch to Zoom", "雙指縮放"),
|
||||
("Canvas Zoom", "縮放畫布"),
|
||||
("Reset canvas", "重設畫布"),
|
||||
("No permission of file transfer", "沒有檔案傳輸權限"),
|
||||
("Note", "備註"),
|
||||
("Connection", "連線"),
|
||||
("Share Screen", "共享螢幕畫面"),
|
||||
("Chat", "聊天訊息"),
|
||||
("Total", "總計"),
|
||||
("items", "個項目"),
|
||||
("Selected", "已選擇"),
|
||||
("Screen Capture", "畫面錄製"),
|
||||
("Input Control", "輸入控制"),
|
||||
("Audio Capture", "音訊錄製"),
|
||||
("File Connection", "檔案連線"),
|
||||
("Screen Connection", "畫面連線"),
|
||||
("Do you accept?", "是否接受?"),
|
||||
("Open System Setting", "開啟系統設定"),
|
||||
("How to get Android input permission?", "如何獲取 Android 的輸入權限?"),
|
||||
("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"),
|
||||
("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"),
|
||||
("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"),
|
||||
("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"),
|
||||
("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"),
|
||||
("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"),
|
||||
("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"),
|
||||
("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"),
|
||||
("Account", "帳號"),
|
||||
("Overwrite", "取代"),
|
||||
("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"),
|
||||
("Quit", "退出"),
|
||||
("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"),
|
||||
("Help", "幫助"),
|
||||
("Failed", "失敗"),
|
||||
("Succeeded", "成功"),
|
||||
("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"),
|
||||
("Unsupported", "不支援"),
|
||||
("Peer denied", "被控端拒絕"),
|
||||
("Please install plugins", "請安裝插件"),
|
||||
("Peer exit", "被控端退出"),
|
||||
("Failed to turn off", "退出失敗"),
|
||||
("Turned off", "退出"),
|
||||
("In privacy mode", "開啟隱私模式"),
|
||||
("Out privacy mode", "退出隱私模式"),
|
||||
("Language", "語言"),
|
||||
("Keep RustDesk background service", "保持 RustDesk 後台服務"),
|
||||
("Ignore Battery Optimizations", "忽略電池最佳化"),
|
||||
("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"),
|
||||
("Start on Boot", "開機自動啟動"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"),
|
||||
("Connection not allowed", "對方不允許連線"),
|
||||
("Legacy mode", "傳統模式"),
|
||||
("Map mode", "1:1 傳輸模式"),
|
||||
("Translate mode", "翻譯模式"),
|
||||
("Use permanent password", "使用固定密碼"),
|
||||
("Use both passwords", "同時使用兩種密碼"),
|
||||
("Set permanent password", "設定固定密碼"),
|
||||
("Enable Remote Restart", "啟用遠端重新啟動"),
|
||||
("Allow remote restart", "允許遠端重新啟動"),
|
||||
("Restart Remote Device", "重新啟動遠端電腦"),
|
||||
("Are you sure you want to restart", "確定要重新啟動"),
|
||||
("Restarting Remote Device", "正在重新啟動遠端裝置"),
|
||||
("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"),
|
||||
("Copied", "已複製"),
|
||||
("Exit Fullscreen", "退出全螢幕"),
|
||||
("Fullscreen", "全螢幕"),
|
||||
("Mobile Actions", "手機操作"),
|
||||
("Select Monitor", "選擇顯示器"),
|
||||
("Control Actions", "控制操作"),
|
||||
("Display Settings", "顯示設定"),
|
||||
("Ratio", "比例"),
|
||||
("Image Quality", "畫質"),
|
||||
("Scroll Style", "滾動樣式"),
|
||||
("Show Menubar", "顯示選單欄"),
|
||||
("Hide Menubar", "隱藏選單欄"),
|
||||
("Direct Connection", "直接連線"),
|
||||
("Relay Connection", "中繼連線"),
|
||||
("Secure Connection", "安全連線"),
|
||||
("Insecure Connection", "非安全連線"),
|
||||
("Scale original", "原始尺寸"),
|
||||
("Scale adaptive", "適應視窗"),
|
||||
("General", "通用"),
|
||||
("Security", "安全"),
|
||||
("Theme", "主題"),
|
||||
("Dark Theme", "黑暗主題"),
|
||||
("Light Theme", "明亮主題"),
|
||||
("Dark", "黑暗"),
|
||||
("Light", "明亮"),
|
||||
("Follow System", "跟隨系統"),
|
||||
("Enable hardware codec", "使用硬體編解碼器"),
|
||||
("Unlock Security Settings", "解鎖安全設定"),
|
||||
("Enable Audio", "允許傳輸音訊"),
|
||||
("Unlock Network Settings", "解鎖網路設定"),
|
||||
("Server", "伺服器"),
|
||||
("Direct IP Access", "IP 直接連線"),
|
||||
("Proxy", "代理"),
|
||||
("Apply", "應用"),
|
||||
("Disconnect all devices?", "中斷所有遠端連線?"),
|
||||
("Clear", "清空"),
|
||||
("Audio Input Device", "音訊輸入裝置"),
|
||||
("Deny remote access", "拒絕遠端存取"),
|
||||
("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"),
|
||||
("Network", "網路"),
|
||||
("Enable RDP", "允許 RDP 訪問"),
|
||||
("Pin menubar", "固定選單欄"),
|
||||
("Unpin menubar", "取消固定選單欄"),
|
||||
("Recording", "錄製"),
|
||||
("Directory", "路徑"),
|
||||
("Automatically record incoming sessions", "自動錄製連入的工作階段"),
|
||||
("Change", "變更"),
|
||||
("Start session recording", "開始錄影"),
|
||||
("Stop session recording", "停止錄影"),
|
||||
("Enable Recording Session", "啟用錄製工作階段"),
|
||||
("Allow recording session", "允許錄製工作階段"),
|
||||
("Enable LAN Discovery", "允許區域網路探索"),
|
||||
("Deny LAN Discovery", "拒絕區域網路探索"),
|
||||
("Write a message", "輸入聊天訊息"),
|
||||
("Prompt", "提示"),
|
||||
("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."),
|
||||
("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"),
|
||||
("Disconnected", "斷開連線"),
|
||||
("Other", "其他"),
|
||||
("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"),
|
||||
("Keyboard Settings", "鍵盤設定"),
|
||||
("Full Access", "完全訪問"),
|
||||
("Screen Share", "僅分享螢幕畫面"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||
("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", "顯示 RustDesk"),
|
||||
("This PC", "此電腦"),
|
||||
("or", "或"),
|
||||
("Continue with", "繼續"),
|
||||
("Elevate", "提升權限"),
|
||||
("Zoom cursor", "縮放游標"),
|
||||
("Accept sessions via password", "只允許透過輸入密碼進行連線"),
|
||||
("Accept sessions via click", "只允許透過點擊接受進行連線"),
|
||||
("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"),
|
||||
("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."),
|
||||
("One-time Password", "一次性密碼"),
|
||||
("Use one-time password", "使用一次性密碼"),
|
||||
("One-time password length", "一次性密碼長度"),
|
||||
("Request access to your device", "請求訪問您的裝置"),
|
||||
("Hide connection management window", "隱藏連線管理視窗"),
|
||||
("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"),
|
||||
("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"),
|
||||
("Right click to select tabs", "右鍵選擇分頁"),
|
||||
("Skipped", "已略過"),
|
||||
("Add to Address Book", "新增到通訊錄"),
|
||||
("Group", "群組"),
|
||||
("Search", "搜尋"),
|
||||
("Closed manually by web console", "被 Web 控制台手動關閉"),
|
||||
("Local keyboard type", "本地鍵盤類型"),
|
||||
("Select local keyboard type", "請選擇本地鍵盤類型"),
|
||||
("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"),
|
||||
("Always use software rendering", "使用軟體渲染"),
|
||||
("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"),
|
||||
("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"),
|
||||
("request_elevation_tip", "如果遠端使用者可以操作電腦,也可以請求提升權限。"),
|
||||
("Wait", "等待"),
|
||||
("Elevation Error", "權限提升失敗"),
|
||||
("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"),
|
||||
("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"),
|
||||
("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"),
|
||||
("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"),
|
||||
("Request Elevation", "請求權限提升"),
|
||||
("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"),
|
||||
("Elevate successfully", "權限提升成功"),
|
||||
("uppercase", "大寫字母"),
|
||||
("lowercase", "小寫字母"),
|
||||
("digit", "數字"),
|
||||
("special character", "特殊字元"),
|
||||
("length>=8", "長度不能小於 8"),
|
||||
("Weak", "弱"),
|
||||
("Medium", "中"),
|
||||
("Strong", "強"),
|
||||
("Switch Sides", "反轉存取方向"),
|
||||
("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"),
|
||||
("Display", "顯示"),
|
||||
("Default View Style", "預設顯示方式"),
|
||||
("Default Scroll Style", "預設滾動方式"),
|
||||
("Default Image Quality", "預設圖像質量"),
|
||||
("Default Codec", "預設編解碼器"),
|
||||
("Bitrate", "位元速率"),
|
||||
("FPS", "幀率"),
|
||||
("Auto", "自動"),
|
||||
("Other Default Options", "其他預設選項"),
|
||||
("Voice call", "語音通話"),
|
||||
("Text chat", "文字聊天"),
|
||||
("Stop voice call", "停止語音通話"),
|
||||
("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"),
|
||||
("Reconnect", "重新連線"),
|
||||
("Codec", "編解碼器"),
|
||||
("Resolution", "解析度"),
|
||||
("No transfers in progress", "沒有正在進行的傳輸"),
|
||||
("Set one-time password length", "設定一次性密碼長度"),
|
||||
("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"),
|
||||
("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"),
|
||||
("RDP Settings", "RDP 設定"),
|
||||
("Sort by", "排序方式"),
|
||||
("New Connection", "新連線"),
|
||||
("Restore", "還原"),
|
||||
("Minimize", "最小化"),
|
||||
("Maximize", "最大化"),
|
||||
("Your Device", "您的裝置"),
|
||||
("empty_recent_tip", "空空如也"),
|
||||
("empty_favorite_tip", "空空如也"),
|
||||
("empty_lan_tip", "空空如也"),
|
||||
("empty_address_book_tip", "空空如也"),
|
||||
("eg: admin", "例如:admin"),
|
||||
("Empty Username", "空使用者帳號"),
|
||||
("Empty Password", "空密碼"),
|
||||
("Me", "我"),
|
||||
("identical_file_tip", "此檔案與對方的檔案一致"),
|
||||
("show_monitors_tip", "在工具列中顯示顯示器"),
|
||||
("View Mode", "瀏覽模式"),
|
||||
("enter_rustdesk_passwd_tip", "輸入 RustDesk 密碼"),
|
||||
("remember_rustdesk_passwd_tip", "記住 RustDesk 密碼"),
|
||||
("login_linux_tip", "登入到遠端 Linux 使用者帳戶"),
|
||||
("login_linux_tooltip_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 介面。"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "狀態"),
|
||||
("Your Desktop", "您的桌面"),
|
||||
("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"),
|
||||
("Password", "密碼"),
|
||||
("Ready", "就緒"),
|
||||
("Established", "已建立"),
|
||||
("connecting_status", "正在連線到 RustDesk 網路 ..."),
|
||||
("Enable Service", "啟用服務"),
|
||||
("Start Service", "啟動服務"),
|
||||
("Service is running", "服務正在執行"),
|
||||
("Service is not running", "服務尚未執行"),
|
||||
("not_ready_status", "尚未就緒,請檢查您的網路連線。"),
|
||||
("Control Remote Desktop", "控制遠端桌面"),
|
||||
("Transfer File", "傳輸檔案"),
|
||||
("Connect", "連線"),
|
||||
("Recent Sessions", "近期的工作階段"),
|
||||
("Address Book", "通訊錄"),
|
||||
("Confirmation", "確認"),
|
||||
("TCP Tunneling", "TCP 通道"),
|
||||
("Remove", "移除"),
|
||||
("Refresh random password", "重新產生隨機密碼"),
|
||||
("Set your own password", "自行設定密碼"),
|
||||
("Enable Keyboard/Mouse", "啟用鍵盤和滑鼠"),
|
||||
("Enable Clipboard", "啟用剪貼簿"),
|
||||
("Enable File Transfer", "啟用檔案傳輸"),
|
||||
("Enable TCP Tunneling", "啟用 TCP 通道"),
|
||||
("IP Whitelisting", "IP 白名單"),
|
||||
("ID/Relay Server", "ID / 轉送伺服器"),
|
||||
("Import Server Config", "匯入伺服器設定"),
|
||||
("Export Server Config", "匯出伺服器設定"),
|
||||
("Import server configuration successfully", "匯入伺服器設定成功"),
|
||||
("Export server configuration successfully", "匯出伺服器設定成功"),
|
||||
("Invalid server configuration", "無效的伺服器設定"),
|
||||
("Clipboard is empty", "剪貼簿是空的"),
|
||||
("Stop service", "停止服務"),
|
||||
("Change ID", "更改 ID"),
|
||||
("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", "隱私權聲明"),
|
||||
("Mute", "靜音"),
|
||||
("Build Date", "構建日期"),
|
||||
("Version", "版本"),
|
||||
("Home", "首頁"),
|
||||
("Audio Input", "音訊輸入"),
|
||||
("Enhancements", "增強功能"),
|
||||
("Hardware Codec", "硬體編解碼器"),
|
||||
("Adaptive Bitrate", "自適應位元速率"),
|
||||
("ID Server", "ID 伺服器"),
|
||||
("Relay Server", "轉送伺服器"),
|
||||
("API Server", "API 伺服器"),
|
||||
("invalid_http", "開頭必須為 http:// 或 https://"),
|
||||
("Invalid IP", "IP 無效"),
|
||||
("Invalid format", "格式無效"),
|
||||
("server_not_support", "伺服器暫不支持"),
|
||||
("Not available", "無法使用"),
|
||||
("Too frequent", "修改過於頻繁,請稍後再試。"),
|
||||
("Cancel", "取消"),
|
||||
("Skip", "跳過"),
|
||||
("Close", "關閉"),
|
||||
("Retry", "重試"),
|
||||
("OK", "確定"),
|
||||
("Password Required", "需要密碼"),
|
||||
("Please enter your password", "請輸入您的密碼"),
|
||||
("Remember password", "記住密碼"),
|
||||
("Wrong Password", "密碼錯誤"),
|
||||
("Do you want to enter again?", "您要重新輸入嗎?"),
|
||||
("Connection Error", "連線錯誤"),
|
||||
("Error", "錯誤"),
|
||||
("Reset by the peer", "對方重設了連線"),
|
||||
("Connecting...", "正在連線 ..."),
|
||||
("Connection in progress. Please wait.", "正在連線,請稍候。"),
|
||||
("Please try 1 minute later", "請於 1 分鐘後再試"),
|
||||
("Login Error", "登入錯誤"),
|
||||
("Successful", "成功"),
|
||||
("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."),
|
||||
("Name", "名稱"),
|
||||
("Type", "類型"),
|
||||
("Modified", "修改時間"),
|
||||
("Size", "大小"),
|
||||
("Show Hidden Files", "顯示隱藏檔案"),
|
||||
("Receive", "接收"),
|
||||
("Send", "傳送"),
|
||||
("Refresh File", "重新整理檔案"),
|
||||
("Local", "本地"),
|
||||
("Remote", "遠端"),
|
||||
("Remote Computer", "遠端電腦"),
|
||||
("Local Computer", "本地電腦"),
|
||||
("Confirm Delete", "確認刪除"),
|
||||
("Delete", "刪除"),
|
||||
("Properties", "屬性"),
|
||||
("Multi Select", "多選"),
|
||||
("Select All", "全選"),
|
||||
("Unselect All", "取消全選"),
|
||||
("Empty Directory", "空資料夾"),
|
||||
("Not an empty directory", "不是一個空資料夾"),
|
||||
("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"),
|
||||
("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"),
|
||||
("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"),
|
||||
("Do this for all conflicts", "套用到其他衝突"),
|
||||
("This is irreversible!", "此操作不可逆!"),
|
||||
("Deleting", "正在刪除 ..."),
|
||||
("files", "檔案"),
|
||||
("Waiting", "正在等候 ..."),
|
||||
("Finished", "已完成"),
|
||||
("Speed", "速度"),
|
||||
("Custom Image Quality", "自訂畫面品質"),
|
||||
("Privacy mode", "隱私模式"),
|
||||
("Block user input", "封鎖使用者輸入"),
|
||||
("Unblock user input", "取消封鎖使用者輸入"),
|
||||
("Adjust Window", "調整視窗"),
|
||||
("Original", "原始"),
|
||||
("Shrink", "縮減"),
|
||||
("Stretch", "延展"),
|
||||
("Scrollbar", "滾動條"),
|
||||
("ScrollAuto", "自動滾動"),
|
||||
("Good image quality", "最佳化畫面品質"),
|
||||
("Balanced", "平衡"),
|
||||
("Optimize reaction time", "最佳化反應時間"),
|
||||
("Custom", "自訂"),
|
||||
("Show remote cursor", "顯示遠端游標"),
|
||||
("Show quality monitor", "顯示質量監測"),
|
||||
("Disable clipboard", "停用剪貼簿"),
|
||||
("Lock after session end", "工作階段結束後鎖定電腦"),
|
||||
("Insert", "插入"),
|
||||
("Insert Lock", "鎖定遠端電腦"),
|
||||
("Refresh", "重新載入"),
|
||||
("ID does not exist", "ID 不存在"),
|
||||
("Failed to connect to rendezvous server", "無法連線到 rendezvous 伺服器"),
|
||||
("Please try later", "請稍候再試"),
|
||||
("Remote desktop is offline", "遠端桌面已離線"),
|
||||
("Key mismatch", "金鑰不符"),
|
||||
("Timeout", "逾時"),
|
||||
("Failed to connect to relay server", "無法連線到轉送伺服器"),
|
||||
("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"),
|
||||
("Failed to connect via relay server", "無法透過轉送伺服器連線"),
|
||||
("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"),
|
||||
("Set Password", "設定密碼"),
|
||||
("OS Password", "作業系統密碼"),
|
||||
("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"),
|
||||
("Click to upgrade", "點擊以升級"),
|
||||
("Click to download", "點擊以下載"),
|
||||
("Click to update", "點擊以更新"),
|
||||
("Configure", "設定"),
|
||||
("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"),
|
||||
("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"),
|
||||
("Installing ...", "正在安裝 ..."),
|
||||
("Install", "安裝"),
|
||||
("Installation", "安裝"),
|
||||
("Installation Path", "安裝路徑"),
|
||||
("Create start menu shortcuts", "新增開始功能表捷徑"),
|
||||
("Create desktop icon", "新增桌面捷徑"),
|
||||
("agreement_tip", "開始安裝即表示接受許可協議"),
|
||||
("Accept and Install", "接受並安裝"),
|
||||
("End-user license agreement", "使用者授權合約"),
|
||||
("Generating ...", "正在產生 ..."),
|
||||
("Your installation is lower version.", "您安裝的版本過舊。"),
|
||||
("not_close_tcp_tip", "使用通道時請不要關閉此視窗"),
|
||||
("Listening ...", "正在等待通道連線 ..."),
|
||||
("Remote Host", "遠端主機"),
|
||||
("Remote Port", "遠端連線端口"),
|
||||
("Action", "操作"),
|
||||
("Add", "新增"),
|
||||
("Local Port", "本機連線端口"),
|
||||
("Local Address", "本機地址"),
|
||||
("Change Local Port", "修改本機連線端口"),
|
||||
("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"),
|
||||
("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"),
|
||||
("The confirmation is not identical.", "兩次輸入不相符"),
|
||||
("Permissions", "權限"),
|
||||
("Accept", "接受"),
|
||||
("Dismiss", "關閉"),
|
||||
("Disconnect", "中斷連線"),
|
||||
("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"),
|
||||
("Allow using clipboard", "允許使用剪貼簿"),
|
||||
("Allow hearing sound", "允許分享音訊"),
|
||||
("Allow file copy and paste", "允許檔案複製和貼上"),
|
||||
("Connected", "已連線"),
|
||||
("Direct and encrypted connection", "加密直接連線"),
|
||||
("Relayed and encrypted connection", "加密轉送連線"),
|
||||
("Direct and unencrypted connection", "未加密直接連線"),
|
||||
("Relayed and unencrypted connection", "未加密轉送連線"),
|
||||
("Enter Remote ID", "輸入遠端 ID"),
|
||||
("Enter your password", "輸入您的密碼"),
|
||||
("Logging in...", "正在登入 ..."),
|
||||
("Enable RDP session sharing", "啟用 RDP 工作階段共享"),
|
||||
("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"),
|
||||
("Enable Direct IP Access", "允許 IP 直接存取"),
|
||||
("Rename", "重新命名"),
|
||||
("Space", "空白"),
|
||||
("Create Desktop Shortcut", "新增桌面捷徑"),
|
||||
("Change Path", "更改路徑"),
|
||||
("Create Folder", "新增資料夾"),
|
||||
("Please enter the folder name", "請輸入資料夾名稱"),
|
||||
("Fix it", "修復"),
|
||||
("Warning", "警告"),
|
||||
("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"),
|
||||
("Reboot required", "需要重新啟動"),
|
||||
("Unsupported display server", "不支援顯示伺服器"),
|
||||
("x11 expected", "預期 x11"),
|
||||
("Port", "端口"),
|
||||
("Settings", "設定"),
|
||||
("Username", "使用者名稱"),
|
||||
("Invalid port", "連線端口無效"),
|
||||
("Closed manually by the peer", "遠端使用者關閉了工作階段"),
|
||||
("Enable remote configuration modification", "允許遠端使用者更改設定"),
|
||||
("Run without install", "跳過安裝直接執行"),
|
||||
("Connect via relay", "中繼連線"),
|
||||
("Always connect via relay", "一律透過轉送連線"),
|
||||
("whitelist_tip", "只有白名單中的 IP 可以存取"),
|
||||
("Login", "登入"),
|
||||
("Verify", "驗證"),
|
||||
("Remember me", "記住我"),
|
||||
("Trust this device", "信任此裝置"),
|
||||
("Verification code", "驗證碼"),
|
||||
("verification_tip", "檢測到新裝置登入,已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"),
|
||||
("Logout", "登出"),
|
||||
("Tags", "標籤"),
|
||||
("Search ID", "搜尋 ID"),
|
||||
("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"),
|
||||
("Add ID", "新增 ID"),
|
||||
("Add Tag", "新增標籤"),
|
||||
("Unselect all tags", "取消選取所有標籤"),
|
||||
("Network error", "網路錯誤"),
|
||||
("Username missed", "缺少使用者名稱"),
|
||||
("Password missed", "缺少密碼"),
|
||||
("Wrong credentials", "提供的登入資訊有誤"),
|
||||
("Edit Tag", "編輯標籤"),
|
||||
("Unremember Password", "忘掉密碼"),
|
||||
("Favorites", "我的最愛"),
|
||||
("Add to Favorites", "新增到我的最愛"),
|
||||
("Remove from Favorites", "從我的最愛中刪除"),
|
||||
("Empty", "空空如也"),
|
||||
("Invalid folder name", "資料夾名稱無效"),
|
||||
("Socks5 Proxy", "Socks5 代理"),
|
||||
("Hostname", "主機名稱"),
|
||||
("Discovered", "已探索"),
|
||||
("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"),
|
||||
("Remote ID", "遠端 ID"),
|
||||
("Paste", "貼上"),
|
||||
("Paste here?", "貼上到這裡?"),
|
||||
("Are you sure to close the connection?", "您確定要關閉連線嗎?"),
|
||||
("Download new version", "下載新版本"),
|
||||
("Touch mode", "觸控模式"),
|
||||
("Mouse mode", "滑鼠模式"),
|
||||
("One-Finger Tap", "單指輕觸"),
|
||||
("Left Mouse", "滑鼠左鍵"),
|
||||
("One-Long Tap", "單指長按"),
|
||||
("Two-Finger Tap", "雙指輕觸"),
|
||||
("Right Mouse", "滑鼠右鍵"),
|
||||
("One-Finger Move", "單指移動"),
|
||||
("Double Tap & Move", "雙擊並移動"),
|
||||
("Mouse Drag", "滑鼠選中拖動"),
|
||||
("Three-Finger vertically", "三指垂直滑動"),
|
||||
("Mouse Wheel", "滑鼠滾輪"),
|
||||
("Two-Finger Move", "雙指移動"),
|
||||
("Canvas Move", "移動畫布"),
|
||||
("Pinch to Zoom", "雙指縮放"),
|
||||
("Canvas Zoom", "縮放畫布"),
|
||||
("Reset canvas", "重設畫布"),
|
||||
("No permission of file transfer", "沒有檔案傳輸權限"),
|
||||
("Note", "備註"),
|
||||
("Connection", "連線"),
|
||||
("Share Screen", "共享螢幕畫面"),
|
||||
("Chat", "聊天訊息"),
|
||||
("Total", "總計"),
|
||||
("items", "個項目"),
|
||||
("Selected", "已選擇"),
|
||||
("Screen Capture", "畫面錄製"),
|
||||
("Input Control", "輸入控制"),
|
||||
("Audio Capture", "音訊錄製"),
|
||||
("File Connection", "檔案連線"),
|
||||
("Screen Connection", "畫面連線"),
|
||||
("Do you accept?", "是否接受?"),
|
||||
("Open System Setting", "開啟系統設定"),
|
||||
("How to get Android input permission?", "如何獲取 Android 的輸入權限?"),
|
||||
("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"),
|
||||
("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"),
|
||||
("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"),
|
||||
("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"),
|
||||
("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"),
|
||||
("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"),
|
||||
("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"),
|
||||
("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"),
|
||||
("Account", "帳號"),
|
||||
("Overwrite", "取代"),
|
||||
("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"),
|
||||
("Quit", "退出"),
|
||||
("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"),
|
||||
("Help", "幫助"),
|
||||
("Failed", "失敗"),
|
||||
("Succeeded", "成功"),
|
||||
("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"),
|
||||
("Unsupported", "不支援"),
|
||||
("Peer denied", "被控端拒絕"),
|
||||
("Please install plugins", "請安裝插件"),
|
||||
("Peer exit", "被控端退出"),
|
||||
("Failed to turn off", "退出失敗"),
|
||||
("Turned off", "退出"),
|
||||
("In privacy mode", "開啟隱私模式"),
|
||||
("Out privacy mode", "退出隱私模式"),
|
||||
("Language", "語言"),
|
||||
("Keep RustDesk background service", "保持 RustDesk 後台服務"),
|
||||
("Ignore Battery Optimizations", "忽略電池最佳化"),
|
||||
("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"),
|
||||
("Start on Boot", "開機自動啟動"),
|
||||
("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"),
|
||||
("Connection not allowed", "對方不允許連線"),
|
||||
("Legacy mode", "傳統模式"),
|
||||
("Map mode", "1:1 傳輸模式"),
|
||||
("Translate mode", "翻譯模式"),
|
||||
("Use permanent password", "使用固定密碼"),
|
||||
("Use both passwords", "同時使用兩種密碼"),
|
||||
("Set permanent password", "設定固定密碼"),
|
||||
("Enable Remote Restart", "啟用遠端重新啟動"),
|
||||
("Allow remote restart", "允許遠端重新啟動"),
|
||||
("Restart Remote Device", "重新啟動遠端電腦"),
|
||||
("Are you sure you want to restart", "確定要重新啟動"),
|
||||
("Restarting Remote Device", "正在重新啟動遠端裝置"),
|
||||
("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"),
|
||||
("Copied", "已複製"),
|
||||
("Exit Fullscreen", "退出全螢幕"),
|
||||
("Fullscreen", "全螢幕"),
|
||||
("Mobile Actions", "手機操作"),
|
||||
("Select Monitor", "選擇顯示器"),
|
||||
("Control Actions", "控制操作"),
|
||||
("Display Settings", "顯示設定"),
|
||||
("Ratio", "比例"),
|
||||
("Image Quality", "畫質"),
|
||||
("Scroll Style", "滾動樣式"),
|
||||
("Show Menubar", "顯示選單欄"),
|
||||
("Hide Menubar", "隱藏選單欄"),
|
||||
("Direct Connection", "直接連線"),
|
||||
("Relay Connection", "中繼連線"),
|
||||
("Secure Connection", "安全連線"),
|
||||
("Insecure Connection", "非安全連線"),
|
||||
("Scale original", "原始尺寸"),
|
||||
("Scale adaptive", "適應視窗"),
|
||||
("General", "通用"),
|
||||
("Security", "安全"),
|
||||
("Theme", "主題"),
|
||||
("Dark Theme", "黑暗主題"),
|
||||
("Light Theme", "明亮主題"),
|
||||
("Dark", "黑暗"),
|
||||
("Light", "明亮"),
|
||||
("Follow System", "跟隨系統"),
|
||||
("Enable hardware codec", "使用硬體編解碼器"),
|
||||
("Unlock Security Settings", "解鎖安全設定"),
|
||||
("Enable Audio", "允許傳輸音訊"),
|
||||
("Unlock Network Settings", "解鎖網路設定"),
|
||||
("Server", "伺服器"),
|
||||
("Direct IP Access", "IP 直接連線"),
|
||||
("Proxy", "代理"),
|
||||
("Apply", "應用"),
|
||||
("Disconnect all devices?", "中斷所有遠端連線?"),
|
||||
("Clear", "清空"),
|
||||
("Audio Input Device", "音訊輸入裝置"),
|
||||
("Deny remote access", "拒絕遠端存取"),
|
||||
("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"),
|
||||
("Network", "網路"),
|
||||
("Enable RDP", "允許 RDP 訪問"),
|
||||
("Pin menubar", "固定選單欄"),
|
||||
("Unpin menubar", "取消固定選單欄"),
|
||||
("Recording", "錄製"),
|
||||
("Directory", "路徑"),
|
||||
("Automatically record incoming sessions", "自動錄製連入的工作階段"),
|
||||
("Change", "變更"),
|
||||
("Start session recording", "開始錄影"),
|
||||
("Stop session recording", "停止錄影"),
|
||||
("Enable Recording Session", "啟用錄製工作階段"),
|
||||
("Allow recording session", "允許錄製工作階段"),
|
||||
("Enable LAN Discovery", "允許區域網路探索"),
|
||||
("Deny LAN Discovery", "拒絕區域網路探索"),
|
||||
("Write a message", "輸入聊天訊息"),
|
||||
("Prompt", "提示"),
|
||||
("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."),
|
||||
("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"),
|
||||
("Disconnected", "斷開連線"),
|
||||
("Other", "其他"),
|
||||
("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"),
|
||||
("Keyboard Settings", "鍵盤設定"),
|
||||
("Full Access", "完全訪問"),
|
||||
("Screen Share", "僅分享螢幕畫面"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
|
||||
("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", "顯示 RustDesk"),
|
||||
("This PC", "此電腦"),
|
||||
("or", "或"),
|
||||
("Continue with", "繼續"),
|
||||
("Elevate", "提升權限"),
|
||||
("Zoom cursor", "縮放游標"),
|
||||
("Accept sessions via password", "只允許透過輸入密碼進行連線"),
|
||||
("Accept sessions via click", "只允許透過點擊接受進行連線"),
|
||||
("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"),
|
||||
("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."),
|
||||
("One-time Password", "一次性密碼"),
|
||||
("Use one-time password", "使用一次性密碼"),
|
||||
("One-time password length", "一次性密碼長度"),
|
||||
("Request access to your device", "請求訪問您的裝置"),
|
||||
("Hide connection management window", "隱藏連線管理視窗"),
|
||||
("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"),
|
||||
("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"),
|
||||
("Right click to select tabs", "右鍵選擇分頁"),
|
||||
("Skipped", "已略過"),
|
||||
("Add to Address Book", "新增到通訊錄"),
|
||||
("Group", "群組"),
|
||||
("Search", "搜尋"),
|
||||
("Closed manually by web console", "被 Web 控制台手動關閉"),
|
||||
("Local keyboard type", "本地鍵盤類型"),
|
||||
("Select local keyboard type", "請選擇本地鍵盤類型"),
|
||||
("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"),
|
||||
("Always use software rendering", "使用軟體渲染"),
|
||||
("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"),
|
||||
("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"),
|
||||
("request_elevation_tip", "如果遠端使用者可以操作電腦,也可以請求提升權限。"),
|
||||
("Wait", "等待"),
|
||||
("Elevation Error", "權限提升失敗"),
|
||||
("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"),
|
||||
("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"),
|
||||
("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"),
|
||||
("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"),
|
||||
("Request Elevation", "請求權限提升"),
|
||||
("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"),
|
||||
("Elevate successfully", "權限提升成功"),
|
||||
("uppercase", "大寫字母"),
|
||||
("lowercase", "小寫字母"),
|
||||
("digit", "數字"),
|
||||
("special character", "特殊字元"),
|
||||
("length>=8", "長度不能小於 8"),
|
||||
("Weak", "弱"),
|
||||
("Medium", "中"),
|
||||
("Strong", "強"),
|
||||
("Switch Sides", "反轉存取方向"),
|
||||
("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"),
|
||||
("Display", "顯示"),
|
||||
("Default View Style", "預設顯示方式"),
|
||||
("Default Scroll Style", "預設滾動方式"),
|
||||
("Default Image Quality", "預設圖像質量"),
|
||||
("Default Codec", "預設編解碼器"),
|
||||
("Bitrate", "位元速率"),
|
||||
("FPS", "幀率"),
|
||||
("Auto", "自動"),
|
||||
("Other Default Options", "其他預設選項"),
|
||||
("Voice call", "語音通話"),
|
||||
("Text chat", "文字聊天"),
|
||||
("Stop voice call", "停止語音通話"),
|
||||
("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"),
|
||||
("Reconnect", "重新連線"),
|
||||
("Codec", "編解碼器"),
|
||||
("Resolution", "解析度"),
|
||||
("No transfers in progress", "沒有正在進行的傳輸"),
|
||||
("Set one-time password length", "設定一次性密碼長度"),
|
||||
("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"),
|
||||
("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"),
|
||||
("RDP Settings", "RDP 設定"),
|
||||
("Sort by", "排序方式"),
|
||||
("New Connection", "新連線"),
|
||||
("Restore", "還原"),
|
||||
("Minimize", "最小化"),
|
||||
("Maximize", "最大化"),
|
||||
("Your Device", "您的裝置"),
|
||||
("empty_recent_tip", "空空如也"),
|
||||
("empty_favorite_tip", "空空如也"),
|
||||
("empty_lan_tip", "空空如也"),
|
||||
("empty_address_book_tip", "空空如也"),
|
||||
("eg: admin", "例如:admin"),
|
||||
("Empty Username", "空使用者帳號"),
|
||||
("Empty Password", "空密碼"),
|
||||
("Me", "我"),
|
||||
("identical_file_tip", "此檔案與對方的檔案一致"),
|
||||
("show_monitors_tip", "在工具列中顯示顯示器"),
|
||||
("View Mode", "瀏覽模式"),
|
||||
("login_linux_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 介面"),
|
||||
("verify_rustdesk_password_tip", "驗證 RustDesk 密碼"),
|
||||
("remember_account_tip", "記住此使用者帳戶"),
|
||||
("os_account_desk_tip", "此使用者帳戶將用於登入遠端作業系統並啟用無頭模式的桌面連線"),
|
||||
("OS Account", "作業系統使用者帳戶"),
|
||||
("another_user_login_title_tip", "另一個使用者已經登入"),
|
||||
("another_user_login_text_tip", "斷開連線"),
|
||||
("xorg_not_found_title_tip", "未找到 Xorg"),
|
||||
("xorg_not_found_text_tip", "請安裝 Xorg"),
|
||||
("no_desktop_title_tip", "沒有可用的桌面"),
|
||||
("no_desktop_text_tip", "請安裝 GNOME 桌面"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("identical_file_tip", ""),
|
||||
("show_monitors_tip", ""),
|
||||
("View Mode", ""),
|
||||
("enter_rustdesk_passwd_tip", ""),
|
||||
("remember_rustdesk_passwd_tip", ""),
|
||||
("login_linux_tip", ""),
|
||||
("verify_rustdesk_password_tip", ""),
|
||||
("remember_account_tip", ""),
|
||||
("os_account_desk_tip", ""),
|
||||
("OS Account", ""),
|
||||
("another_user_login_title_tip", ""),
|
||||
("another_user_login_text_tip", ""),
|
||||
("xorg_not_found_title_tip", ""),
|
||||
("xorg_not_found_text_tip", ""),
|
||||
("no_desktop_title_tip", ""),
|
||||
("no_desktop_text_tip", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
pub mod platform;
|
||||
mod keyboard;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use keyboard::keycode_to_rdev_key;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service};
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
/// cbindgen:ignore
|
||||
|
||||
@ -4,26 +4,13 @@ use hbb_common::ResultType;
|
||||
use license::*;
|
||||
|
||||
fn gen_name(lic: &License) -> ResultType<String> {
|
||||
let tmp = serde_json::to_vec::<License>(lic)?;
|
||||
let tmp = URL_SAFE_NO_PAD.encode(&tmp);
|
||||
let tmp: String = tmp.chars().rev().collect();
|
||||
Ok(tmp)
|
||||
let tmp = URL_SAFE_NO_PAD.encode(&serde_json::to_vec(lic)?);
|
||||
Ok(tmp.chars().rev().collect())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = Vec::new();
|
||||
let mut i = 0;
|
||||
for arg in std::env::args() {
|
||||
if i > 0 {
|
||||
args.push(arg);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let api = if args.len() < 3 {
|
||||
"".to_owned()
|
||||
} else {
|
||||
args[2].clone()
|
||||
};
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let api = args.get(2).cloned().unwrap_or_default();
|
||||
if args.len() >= 2 {
|
||||
println!(
|
||||
"rustdesk-licensed-{}.exe",
|
||||
|
||||
@ -225,6 +225,16 @@ fn stop_rustdesk_servers() {
|
||||
));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stop_subprocess() {
|
||||
let _ = run_cmds(&format!(
|
||||
r##"ps -ef | grep '/etc/rustdesk/xorg.conf' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##,
|
||||
));
|
||||
let _ = run_cmds(&format!(
|
||||
r##"ps -ef | grep -E 'rustdesk +--cm-no-ui' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##,
|
||||
));
|
||||
}
|
||||
|
||||
fn should_start_server(
|
||||
try_x11: bool,
|
||||
uid: &mut String,
|
||||
@ -295,6 +305,7 @@ fn force_stop_server() {
|
||||
|
||||
pub fn start_os_service() {
|
||||
stop_rustdesk_servers();
|
||||
stop_subprocess();
|
||||
start_uinput_service();
|
||||
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
@ -329,6 +340,7 @@ pub fn start_os_service() {
|
||||
&mut last_restart,
|
||||
&mut server,
|
||||
) {
|
||||
stop_subprocess();
|
||||
force_stop_server();
|
||||
start_server(None, &mut server);
|
||||
}
|
||||
@ -345,6 +357,7 @@ pub fn start_os_service() {
|
||||
&mut last_restart,
|
||||
&mut user_server,
|
||||
) {
|
||||
stop_subprocess();
|
||||
force_stop_server();
|
||||
start_server(
|
||||
Some((desktop.uid.clone(), desktop.username.clone())),
|
||||
@ -454,9 +467,10 @@ pub fn get_env_var(k: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Headless is enabled, always return true.
|
||||
pub fn is_prelogin() -> bool {
|
||||
let (uid, uname) = get_active_user_id_name();
|
||||
uid.len() >= 4 || uname == "root"
|
||||
let n = get_active_userid().len();
|
||||
n < 4 && n > 1
|
||||
}
|
||||
|
||||
pub fn is_root() -> bool {
|
||||
@ -769,6 +783,7 @@ mod desktop {
|
||||
pub protocal: String,
|
||||
pub display: String,
|
||||
pub xauth: String,
|
||||
pub is_rustdesk_subprocess: bool,
|
||||
}
|
||||
|
||||
impl Desktop {
|
||||
@ -784,7 +799,7 @@ mod desktop {
|
||||
|
||||
#[inline]
|
||||
pub fn is_headless(&self) -> bool {
|
||||
self.sid.is_empty()
|
||||
self.sid.is_empty() || self.is_rustdesk_subprocess
|
||||
}
|
||||
|
||||
fn get_display(&mut self) {
|
||||
@ -901,6 +916,16 @@ mod desktop {
|
||||
last
|
||||
}
|
||||
|
||||
fn set_is_subprocess(&mut self) {
|
||||
self.is_rustdesk_subprocess = false;
|
||||
let cmd = "ps -ef | grep 'rustdesk/xorg.conf' | grep -v grep | wc -l";
|
||||
if let Ok(res) = run_cmds(cmd) {
|
||||
if res.trim() != "0" {
|
||||
self.is_rustdesk_subprocess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) {
|
||||
if !self.sid.is_empty() && is_active(&self.sid) {
|
||||
return;
|
||||
@ -909,6 +934,7 @@ mod desktop {
|
||||
let seat0_values = get_values_of_seat0(&[0, 1, 2]);
|
||||
if seat0_values[0].is_empty() {
|
||||
*self = Self::default();
|
||||
self.is_rustdesk_subprocess = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -919,11 +945,13 @@ mod desktop {
|
||||
if self.is_login_wayland() {
|
||||
self.display = "".to_owned();
|
||||
self.xauth = "".to_owned();
|
||||
self.is_rustdesk_subprocess = false;
|
||||
return;
|
||||
}
|
||||
|
||||
self.get_display();
|
||||
self.get_xauth();
|
||||
self.set_is_subprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
733
src/platform/linux_desktop_manager.rs
Normal file
733
src/platform/linux_desktop_manager.rs
Normal file
@ -0,0 +1,733 @@
|
||||
use super::{linux::*, ResultType};
|
||||
use crate::server::{
|
||||
LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER,
|
||||
LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND,
|
||||
LOGIN_MSG_DESKTOP_XSESSION_FAILED,
|
||||
};
|
||||
use hbb_common::{allow_err, bail, log, rand::prelude::*, tokio::time};
|
||||
use pam;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
os::unix::process::CommandExt,
|
||||
path::Path,
|
||||
process::{Child, Command},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{sync_channel, SyncSender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use users::{get_user_by_name, os::unix::UserExt, User};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DESKTOP_RUNNING: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
|
||||
static ref DESKTOP_MANAGER: Arc<Mutex<Option<DesktopManager>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DesktopManager {
|
||||
seat0_username: String,
|
||||
seat0_display_server: String,
|
||||
child_username: String,
|
||||
child_exit: Arc<AtomicBool>,
|
||||
is_child_running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
fn check_desktop_manager() {
|
||||
let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap();
|
||||
if let Some(desktop_manager) = &mut (*desktop_manager) {
|
||||
if desktop_manager.is_child_running.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
desktop_manager.child_exit.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// --server process
|
||||
pub fn start_xdesktop() {
|
||||
std::thread::spawn(|| {
|
||||
*DESKTOP_MANAGER.lock().unwrap() = Some(DesktopManager::new());
|
||||
|
||||
let interval = time::Duration::from_millis(super::SERVICE_INTERVAL);
|
||||
DESKTOP_RUNNING.store(true, Ordering::SeqCst);
|
||||
while DESKTOP_RUNNING.load(Ordering::SeqCst) {
|
||||
check_desktop_manager();
|
||||
std::thread::sleep(interval);
|
||||
}
|
||||
log::info!("xdesktop child thread exit");
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stop_xdesktop() {
|
||||
DESKTOP_RUNNING.store(false, Ordering::SeqCst);
|
||||
*DESKTOP_MANAGER.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
fn detect_headless() -> Option<&'static str> {
|
||||
match run_cmds(&format!("which {}", DesktopManager::get_xorg())) {
|
||||
Ok(output) => {
|
||||
if output.trim().is_empty() {
|
||||
return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
match run_cmds("ls /usr/share/xsessions/") {
|
||||
Ok(output) => {
|
||||
if output.trim().is_empty() {
|
||||
return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
|
||||
if _username.is_empty() {
|
||||
let username = get_username();
|
||||
if username.is_empty() {
|
||||
if let Some(msg) = detect_headless() {
|
||||
msg
|
||||
} else {
|
||||
LOGIN_MSG_DESKTOP_SESSION_NOT_READY
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
.to_owned()
|
||||
} else {
|
||||
let username = get_username();
|
||||
if username == _username {
|
||||
// No need to verify password here.
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
if let Some(msg) = detect_headless() {
|
||||
return msg.to_owned();
|
||||
}
|
||||
|
||||
match try_start_x_session(_username, _passsword) {
|
||||
Ok((username, x11_ready)) => {
|
||||
if x11_ready {
|
||||
if _username != username {
|
||||
LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER.to_owned()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
} else {
|
||||
LOGIN_MSG_DESKTOP_SESSION_NOT_READY.to_owned()
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to start xsession {}", e);
|
||||
LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> {
|
||||
let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap();
|
||||
if let Some(desktop_manager) = &mut (*desktop_manager) {
|
||||
if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() {
|
||||
return Ok((seat0_username, true));
|
||||
}
|
||||
|
||||
let _ = desktop_manager.try_start_x_session(username, password)?;
|
||||
log::debug!(
|
||||
"try_start_x_session, username: {}, {:?}",
|
||||
&username,
|
||||
&desktop_manager
|
||||
);
|
||||
Ok((
|
||||
desktop_manager.child_username.clone(),
|
||||
desktop_manager.is_running(),
|
||||
))
|
||||
} else {
|
||||
bail!(crate::server::LOGIN_MSG_DESKTOP_NOT_INITED);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_headless() -> bool {
|
||||
DESKTOP_MANAGER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map_or(false, |manager| {
|
||||
manager.get_supported_display_seat0_username().is_none()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_username() -> String {
|
||||
match &*DESKTOP_MANAGER.lock().unwrap() {
|
||||
Some(manager) => {
|
||||
if let Some(seat0_username) = manager.get_supported_display_seat0_username() {
|
||||
seat0_username
|
||||
} else {
|
||||
if manager.is_running() && !manager.child_username.is_empty() {
|
||||
manager.child_username.clone()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DesktopManager {
|
||||
fn drop(&mut self) {
|
||||
self.stop_children();
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopManager {
|
||||
fn fatal_exit() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let mut seat0_username = "".to_owned();
|
||||
let mut seat0_display_server = "".to_owned();
|
||||
let seat0_values = get_values_of_seat0(&[0, 2]);
|
||||
if !seat0_values[0].is_empty() {
|
||||
seat0_username = seat0_values[1].clone();
|
||||
seat0_display_server = get_display_server_of_session(&seat0_values[0]);
|
||||
}
|
||||
Self {
|
||||
seat0_username,
|
||||
seat0_display_server,
|
||||
child_username: "".to_owned(),
|
||||
child_exit: Arc::new(AtomicBool::new(true)),
|
||||
is_child_running: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_supported_display_seat0_username(&self) -> Option<String> {
|
||||
if is_gdm_user(&self.seat0_username) && self.seat0_display_server == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
None
|
||||
} else if self.seat0_username.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.seat0_username.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_xauth() -> String {
|
||||
let xauth = get_env_var("XAUTHORITY");
|
||||
if xauth.is_empty() {
|
||||
"/tmp/.Xauthority".to_owned()
|
||||
} else {
|
||||
xauth
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_running(&self) -> bool {
|
||||
self.is_child_running.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> {
|
||||
match get_user_by_name(username) {
|
||||
Some(userinfo) => {
|
||||
let mut client = pam::Client::with_password(pam_get_service_name())?;
|
||||
client
|
||||
.conversation_mut()
|
||||
.set_credentials(username, password);
|
||||
match client.authenticate() {
|
||||
Ok(_) => {
|
||||
if self.is_running() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match self.start_x_session(&userinfo, username, password) {
|
||||
Ok(_) => {
|
||||
log::info!("Succeeded to start x11");
|
||||
self.child_username = username.to_string();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("failed to start x session, {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("failed to check user pass for {}, {}", username, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
bail!("failed to get userinfo of {}", username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The logic mainly fron https://github.com/neutrinolabs/xrdp/blob/34fe9b60ebaea59e8814bbc3ca5383cabaa1b869/sesman/session.c#L334.
|
||||
fn get_avail_display() -> ResultType<u32> {
|
||||
let display_range = 0..51;
|
||||
for i in display_range.clone() {
|
||||
if Self::is_x_server_running(i) {
|
||||
continue;
|
||||
}
|
||||
return Ok(i);
|
||||
}
|
||||
bail!("No avaliable display found in range {:?}", display_range)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_x_server_running(display: u32) -> bool {
|
||||
Path::new(&format!("/tmp/.X11-unix/X{}", display)).exists()
|
||||
|| Path::new(&format!("/tmp/.X{}-lock", display)).exists()
|
||||
}
|
||||
|
||||
fn start_x_session(
|
||||
&mut self,
|
||||
userinfo: &User,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> ResultType<()> {
|
||||
self.stop_children();
|
||||
|
||||
let display_num = Self::get_avail_display()?;
|
||||
// "xServer_ip:display_num.screen_num"
|
||||
|
||||
let uid = userinfo.uid();
|
||||
let gid = userinfo.primary_group_id();
|
||||
let envs = HashMap::from([
|
||||
("SHELL", userinfo.shell().to_string_lossy().to_string()),
|
||||
("PATH", "/sbin:/bin:/usr/bin:/usr/local/bin".to_owned()),
|
||||
("USER", username.to_string()),
|
||||
("UID", userinfo.uid().to_string()),
|
||||
("HOME", userinfo.home_dir().to_string_lossy().to_string()),
|
||||
(
|
||||
"XDG_RUNTIME_DIR",
|
||||
format!("/run/user/{}", userinfo.uid().to_string()),
|
||||
),
|
||||
// ("DISPLAY", self.display.clone()),
|
||||
// ("XAUTHORITY", self.xauth.clone()),
|
||||
// (ENV_DESKTOP_PROTOCAL, XProtocal::X11.to_string()),
|
||||
]);
|
||||
self.child_exit.store(false, Ordering::SeqCst);
|
||||
let is_child_running = self.is_child_running.clone();
|
||||
|
||||
let (tx_res, rx_res) = sync_channel(1);
|
||||
let password = password.to_string();
|
||||
let username = username.to_string();
|
||||
// start x11
|
||||
std::thread::spawn(move || {
|
||||
match Self::start_x_session_thread(
|
||||
tx_res.clone(),
|
||||
is_child_running,
|
||||
uid,
|
||||
gid,
|
||||
display_num,
|
||||
username,
|
||||
password,
|
||||
envs,
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to start x session thread");
|
||||
allow_err!(tx_res.send(format!("Failed to start x session thread, {}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// wait x11
|
||||
match rx_res.recv_timeout(Duration::from_millis(10_000)) {
|
||||
Ok(res) => {
|
||||
if res == "" {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(res)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("Failed to recv x11 result {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn display_from_num(num: u32) -> String {
|
||||
format!(":{num}")
|
||||
}
|
||||
|
||||
fn start_x_session_thread(
|
||||
tx_res: SyncSender<String>,
|
||||
is_child_running: Arc<AtomicBool>,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
display_num: u32,
|
||||
username: String,
|
||||
password: String,
|
||||
envs: HashMap<&str, String>,
|
||||
) -> ResultType<()> {
|
||||
let mut client = pam::Client::with_password(pam_get_service_name())?;
|
||||
client
|
||||
.conversation_mut()
|
||||
.set_credentials(&username, &password);
|
||||
client.authenticate()?;
|
||||
|
||||
client.set_item(pam::PamItemType::TTY, &Self::display_from_num(display_num))?;
|
||||
client.open_session()?;
|
||||
|
||||
// fixme: FreeBSD kernel needs to login here.
|
||||
// see: https://github.com/neutrinolabs/xrdp/blob/a64573b596b5fb07ca3a51590c5308d621f7214e/sesman/session.c#L556
|
||||
|
||||
let (child_xorg, child_wm) = Self::start_x11(uid, gid, username, display_num, &envs)?;
|
||||
is_child_running.store(true, Ordering::SeqCst);
|
||||
|
||||
log::info!("Start xorg and wm done, notify and wait xtop x11");
|
||||
allow_err!(tx_res.send("".to_owned()));
|
||||
|
||||
Self::wait_stop_x11(child_xorg, child_wm);
|
||||
log::info!("Wait x11 stop done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_xorg_exit(child_xorg: &mut Child) -> ResultType<String> {
|
||||
if let Ok(_) = child_xorg.kill() {
|
||||
for _ in 0..3 {
|
||||
match child_xorg.try_wait() {
|
||||
Ok(Some(status)) => return Ok(format!("Xorg exit with {}", status)),
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
// fatal error
|
||||
log::error!("Failed to wait xorg process, {}", e);
|
||||
bail!("Failed to wait xorg process, {}", e)
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1_000));
|
||||
}
|
||||
log::error!("Failed to wait xorg process, not exit");
|
||||
bail!("Failed to wait xorg process, not exit")
|
||||
} else {
|
||||
Ok("Xorg is already exited".to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn add_xauth_cookie(
|
||||
file: &str,
|
||||
display: &str,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
envs: &HashMap<&str, String>,
|
||||
) -> ResultType<()> {
|
||||
let randstr = (0..16)
|
||||
.map(|_| format!("{:02x}", random::<u8>()))
|
||||
.collect::<String>();
|
||||
let output = Command::new("xauth")
|
||||
.uid(uid)
|
||||
.gid(gid)
|
||||
.envs(envs)
|
||||
.args(vec!["-q", "-f", file, "add", display, ".", &randstr])
|
||||
.output()?;
|
||||
// xauth run success, even the following error occurs.
|
||||
// Ok(Output { status: ExitStatus(unix_wait_status(0)), stdout: "", stderr: "xauth: file .Xauthority does not exist\n" })
|
||||
let errmsg = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
if !errmsg.is_empty() {
|
||||
if !errmsg.contains("does not exist") {
|
||||
bail!("Failed to launch xauth, {}", errmsg)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_x_server_running(pid: u32, display_num: u32, max_wait_secs: u64) -> ResultType<()> {
|
||||
let wait_begin = Instant::now();
|
||||
loop {
|
||||
if run_cmds(&format!("ls /proc/{}", pid))?.is_empty() {
|
||||
bail!("X server exit");
|
||||
}
|
||||
|
||||
if Self::is_x_server_running(display_num) {
|
||||
return Ok(());
|
||||
}
|
||||
if wait_begin.elapsed().as_secs() > max_wait_secs {
|
||||
bail!("Failed to wait xserver after {} seconds", max_wait_secs);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
}
|
||||
}
|
||||
|
||||
fn start_x11(
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
username: String,
|
||||
display_num: u32,
|
||||
envs: &HashMap<&str, String>,
|
||||
) -> ResultType<(Child, Child)> {
|
||||
log::debug!("envs of user {}: {:?}", &username, &envs);
|
||||
|
||||
let xauth = Self::get_xauth();
|
||||
let display = Self::display_from_num(display_num);
|
||||
|
||||
Self::add_xauth_cookie(&xauth, &display, uid, gid, &envs)?;
|
||||
|
||||
// Start Xorg
|
||||
let mut child_xorg = Self::start_x_server(&xauth, &display, uid, gid, &envs)?;
|
||||
|
||||
log::info!("xorg started, wait 10 secs to ensuer x server is running");
|
||||
|
||||
let max_wait_secs = 10;
|
||||
// wait x server running
|
||||
if let Err(e) = Self::wait_x_server_running(child_xorg.id(), display_num, max_wait_secs) {
|
||||
match Self::wait_xorg_exit(&mut child_xorg) {
|
||||
Ok(msg) => log::info!("{}", msg),
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Self::fatal_exit();
|
||||
}
|
||||
}
|
||||
bail!(e)
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"xorg is running, start x window manager with DISPLAY: {}, XAUTHORITY: {}",
|
||||
&display,
|
||||
&xauth
|
||||
);
|
||||
|
||||
std::env::set_var("DISPLAY", &display);
|
||||
std::env::set_var("XAUTHORITY", &xauth);
|
||||
// start window manager (startwm.sh)
|
||||
let child_wm = match Self::start_x_window_manager(uid, gid, &envs) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
match Self::wait_xorg_exit(&mut child_xorg) {
|
||||
Ok(msg) => log::info!("{}", msg),
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Self::fatal_exit();
|
||||
}
|
||||
}
|
||||
bail!(e)
|
||||
}
|
||||
};
|
||||
log::info!("x window manager is started");
|
||||
|
||||
Ok((child_xorg, child_wm))
|
||||
}
|
||||
|
||||
fn try_wait_x11_child_exit(child_xorg: &mut Child, child_wm: &mut Child) -> bool {
|
||||
match child_xorg.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
log::info!("Xorg exit with {}", status);
|
||||
return true;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => log::error!("Failed to wait xorg process, {}", e),
|
||||
}
|
||||
|
||||
match child_wm.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)"
|
||||
log::info!("wm exit with {}", status);
|
||||
return true;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => log::error!("Failed to wait xorg process, {}", e),
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn wait_x11_children_exit(child_xorg: &mut Child, child_wm: &mut Child) {
|
||||
log::debug!("Try kill child process xorg");
|
||||
if let Ok(_) = child_xorg.kill() {
|
||||
let mut exited = false;
|
||||
for _ in 0..2 {
|
||||
match child_xorg.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
log::info!("Xorg exit with {}", status);
|
||||
exited = true;
|
||||
break;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to wait xorg process, {}", e);
|
||||
Self::fatal_exit();
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1_000));
|
||||
}
|
||||
if !exited {
|
||||
log::error!("Failed to wait child xorg, after kill()");
|
||||
// try kill -9?
|
||||
}
|
||||
}
|
||||
log::debug!("Try kill child process wm");
|
||||
if let Ok(_) = child_wm.kill() {
|
||||
let mut exited = false;
|
||||
for _ in 0..2 {
|
||||
match child_wm.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)"
|
||||
log::info!("wm exit with {}", status);
|
||||
exited = true;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to wait wm process, {}", e);
|
||||
Self::fatal_exit();
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1_000));
|
||||
}
|
||||
if !exited {
|
||||
log::error!("Failed to wait child xorg, after kill()");
|
||||
// try kill -9?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_wait_stop_x11(child_xorg: &mut Child, child_wm: &mut Child) -> bool {
|
||||
let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap();
|
||||
let mut exited = true;
|
||||
if let Some(desktop_manager) = &mut (*desktop_manager) {
|
||||
if desktop_manager.child_exit.load(Ordering::SeqCst) {
|
||||
exited = true;
|
||||
} else {
|
||||
exited = Self::try_wait_x11_child_exit(child_xorg, child_wm);
|
||||
}
|
||||
if exited {
|
||||
log::debug!("Wait x11 children exiting");
|
||||
Self::wait_x11_children_exit(child_xorg, child_wm);
|
||||
desktop_manager
|
||||
.is_child_running
|
||||
.store(false, Ordering::SeqCst);
|
||||
desktop_manager.child_exit.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
exited
|
||||
}
|
||||
|
||||
fn wait_stop_x11(mut child_xorg: Child, mut child_wm: Child) {
|
||||
loop {
|
||||
if Self::try_wait_stop_x11(&mut child_xorg, &mut child_wm) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_xorg() -> &'static str {
|
||||
// Fedora 26 or later
|
||||
let xorg = "/usr/libexec/Xorg";
|
||||
if Path::new(xorg).is_file() {
|
||||
return xorg;
|
||||
}
|
||||
// Debian 9 or later
|
||||
let xorg = "/usr/lib/xorg/Xorg";
|
||||
if Path::new(xorg).is_file() {
|
||||
return xorg;
|
||||
}
|
||||
// Ubuntu 16.04 or later
|
||||
let xorg = "/usr/lib/xorg/Xorg";
|
||||
if Path::new(xorg).is_file() {
|
||||
return xorg;
|
||||
}
|
||||
// Arch Linux
|
||||
let xorg = "/usr/lib/xorg-server/Xorg";
|
||||
if Path::new(xorg).is_file() {
|
||||
return xorg;
|
||||
}
|
||||
// Arch Linux
|
||||
let xorg = "/usr/lib/Xorg";
|
||||
if Path::new(xorg).is_file() {
|
||||
return xorg;
|
||||
}
|
||||
// CentOS 7 /usr/bin/Xorg or param=Xorg
|
||||
|
||||
log::warn!("Failed to find xorg, use default Xorg.\n Please add \"allowed_users=anybody\" to \"/etc/X11/Xwrapper.config\".");
|
||||
"Xorg"
|
||||
}
|
||||
|
||||
fn start_x_server(
|
||||
xauth: &str,
|
||||
display: &str,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
envs: &HashMap<&str, String>,
|
||||
) -> ResultType<Child> {
|
||||
let xorg = Self::get_xorg();
|
||||
log::info!("Use xorg: {}", &xorg);
|
||||
match Command::new(xorg)
|
||||
.envs(envs)
|
||||
.uid(uid)
|
||||
.gid(gid)
|
||||
.args(vec![
|
||||
"-noreset",
|
||||
"+extension",
|
||||
"GLX",
|
||||
"+extension",
|
||||
"RANDR",
|
||||
"+extension",
|
||||
"RENDER",
|
||||
//"-logfile",
|
||||
//"/tmp/RustDesk_xorg.log",
|
||||
"-config",
|
||||
"/etc/rustdesk/xorg.conf",
|
||||
"-auth",
|
||||
xauth,
|
||||
display,
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) => {
|
||||
bail!("Failed to start Xorg with display {}, {}", display, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_x_window_manager(
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
envs: &HashMap<&str, String>,
|
||||
) -> ResultType<Child> {
|
||||
match Command::new("/etc/rustdesk/startwm.sh")
|
||||
.envs(envs)
|
||||
.uid(uid)
|
||||
.gid(gid)
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) => {
|
||||
bail!("Failed to start window manager, {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_children(&mut self) {
|
||||
self.child_exit.store(true, Ordering::SeqCst);
|
||||
for _i in 1..10 {
|
||||
if !self.is_child_running.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL));
|
||||
}
|
||||
if self.is_child_running.load(Ordering::SeqCst) {
|
||||
log::warn!("xdesktop child is still running!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pam_get_service_name() -> &'static str {
|
||||
if Path::new("/etc/pam.d/rustdesk").is_file() {
|
||||
"rustdesk"
|
||||
} else {
|
||||
"gdm"
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,9 @@ pub mod delegate;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
pub mod linux_desktop_manager;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::{message_proto::CursorData, ResultType};
|
||||
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
|
||||
@ -37,7 +40,7 @@ pub fn breakdown_callback() {
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::input_service::clear_remapped_keycode();
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::input_service::release_modifiers();
|
||||
crate::input_service::release_device_modifiers();
|
||||
}
|
||||
|
||||
// Android
|
||||
|
||||
@ -9,16 +9,17 @@ use hbb_common::{
|
||||
message_proto::Resolution,
|
||||
sleep, timeout, tokio,
|
||||
};
|
||||
use std::io::prelude::*;
|
||||
use std::ptr::null_mut;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsString,
|
||||
fs, io, mem,
|
||||
fs, io,
|
||||
io::prelude::*,
|
||||
mem,
|
||||
os::windows::process::CommandExt,
|
||||
path::PathBuf,
|
||||
path::*,
|
||||
ptr::null_mut,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
collections::HashMap
|
||||
};
|
||||
use winapi::{
|
||||
ctypes::c_void,
|
||||
@ -717,7 +718,7 @@ pub fn is_share_rdp() -> bool {
|
||||
}
|
||||
|
||||
pub fn set_share_rdp(enable: bool) {
|
||||
let (subkey, _, _, _) = get_install_info();
|
||||
let (subkey, _, _, _, _) = get_install_info();
|
||||
let cmd = format!(
|
||||
"reg add {} /f /v share_rdp /t REG_SZ /d \"{}\"",
|
||||
subkey,
|
||||
@ -810,11 +811,11 @@ fn get_valid_subkey() -> String {
|
||||
return get_subkey(&app_name, false);
|
||||
}
|
||||
|
||||
pub fn get_install_info() -> (String, String, String, String) {
|
||||
pub fn get_install_info() -> (String, String, String, String, String) {
|
||||
get_install_info_with_subkey(get_valid_subkey())
|
||||
}
|
||||
|
||||
fn get_default_install_info() -> (String, String, String, String) {
|
||||
fn get_default_install_info() -> (String, String, String, String, String) {
|
||||
get_install_info_with_subkey(get_subkey(&crate::get_app_name(), false))
|
||||
}
|
||||
|
||||
@ -883,7 +884,7 @@ pub fn check_update_broker_process() -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String) {
|
||||
fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String, String) {
|
||||
let mut path = get_reg_of(&subkey, "InstallLocation");
|
||||
if path.is_empty() {
|
||||
path = get_default_install_path();
|
||||
@ -894,26 +895,32 @@ fn get_install_info_with_subkey(subkey: String) -> (String, String, String, Stri
|
||||
crate::get_app_name()
|
||||
);
|
||||
let exe = format!("{}\\{}.exe", path, crate::get_app_name());
|
||||
(subkey, path, start_menu, exe)
|
||||
let dll = format!("{}\\sciter.dll", path);
|
||||
(subkey, path, start_menu, exe, dll)
|
||||
}
|
||||
|
||||
pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String {
|
||||
pub fn copy_raw_cmd(src_raw: &str, _raw: &str, _path: &str) -> String {
|
||||
#[cfg(feature = "flutter")]
|
||||
let main_exe = format!(
|
||||
let main_raw = format!(
|
||||
"XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z",
|
||||
PathBuf::from(src_exe)
|
||||
PathBuf::from(src_raw)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
path
|
||||
_path
|
||||
);
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let main_exe = format!(
|
||||
"copy /Y \"{src_exe}\" \"{exe}\"",
|
||||
src_exe = src_exe,
|
||||
exe = _exe
|
||||
let main_raw = format!(
|
||||
"copy /Y \"{src_raw}\" \"{raw}\"",
|
||||
src_raw = src_raw,
|
||||
raw = _raw
|
||||
);
|
||||
return main_raw;
|
||||
}
|
||||
|
||||
pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> String {
|
||||
let main_exe = copy_raw_cmd(src_exe, exe, path);
|
||||
|
||||
return format!(
|
||||
"
|
||||
@ -929,8 +936,20 @@ pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn update_me() -> ResultType<()> {
|
||||
let (_, path, _, exe) = get_install_info();
|
||||
let (_, path, _, exe, _dll) = get_install_info();
|
||||
let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let src_dll = std::env::current_exe()?
|
||||
.parent()
|
||||
.unwrap_or(&Path::new(&get_default_install_path()))
|
||||
.join("sciter.dll")
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
#[cfg(feature = "flutter")]
|
||||
let copy_dll = "".to_string();
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let copy_dll = copy_raw_cmd(&src_dll, &_dll, &path);
|
||||
let cmds = format!(
|
||||
"
|
||||
chcp 65001
|
||||
@ -938,10 +957,12 @@ pub fn update_me() -> ResultType<()> {
|
||||
taskkill /F /IM {broker_exe}
|
||||
taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\"
|
||||
{copy_exe}
|
||||
{copy_dll}
|
||||
sc start {app_name}
|
||||
{lic}
|
||||
",
|
||||
copy_exe = copy_exe_cmd(&src_exe, &exe, &path),
|
||||
copy_dll = copy_dll,
|
||||
broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE,
|
||||
app_name = crate::get_app_name(),
|
||||
lic = register_licence(),
|
||||
@ -980,12 +1001,14 @@ fn get_after_install(exe: &str) -> String {
|
||||
pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> {
|
||||
let uninstall_str = get_uninstall(false);
|
||||
let mut path = path.trim_end_matches('\\').to_owned();
|
||||
let (subkey, _path, start_menu, exe) = get_default_install_info();
|
||||
let (subkey, _path, start_menu, exe, dll) = get_default_install_info();
|
||||
let mut exe = exe;
|
||||
let mut _dll = dll;
|
||||
if path.is_empty() {
|
||||
path = _path;
|
||||
} else {
|
||||
exe = exe.replace(&_path, &path);
|
||||
_dll = _dll.replace(&_path, &path);
|
||||
}
|
||||
let mut version_major = "0";
|
||||
let mut version_minor = "0";
|
||||
@ -1109,6 +1132,18 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name}
|
||||
app_name = crate::get_app_name(),
|
||||
);
|
||||
let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string();
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let src_dll = std::env::current_exe()?
|
||||
.parent()
|
||||
.unwrap_or(&Path::new(&get_default_install_path()))
|
||||
.join("sciter.dll")
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
#[cfg(feature = "flutter")]
|
||||
let copy_dll = "".to_string();
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let copy_dll = copy_raw_cmd(&src_dll, &_dll, &path);
|
||||
|
||||
let install_cert = if options.contains("driverCert") {
|
||||
format!("\"{}\" --install-cert \"RustDeskIddDriver.cer\"", src_exe)
|
||||
@ -1122,6 +1157,7 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name}
|
||||
chcp 65001
|
||||
md \"{path}\"
|
||||
{copy_exe}
|
||||
{copy_dll}
|
||||
reg add {subkey} /f
|
||||
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
|
||||
reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\"
|
||||
@ -1181,6 +1217,7 @@ sc delete {app_name}
|
||||
&dels
|
||||
},
|
||||
copy_exe = copy_exe_cmd(&src_exe, &exe, &path),
|
||||
copy_dll = copy_dll
|
||||
);
|
||||
run_cmds(cmds, debug, "install")?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
@ -1193,7 +1230,7 @@ sc delete {app_name}
|
||||
}
|
||||
|
||||
pub fn run_after_install() -> ResultType<()> {
|
||||
let (_, _, _, exe) = get_install_info();
|
||||
let (_, _, _, exe, _) = get_install_info();
|
||||
run_cmds(get_after_install(&exe), true, "after_install")
|
||||
}
|
||||
|
||||
@ -1227,7 +1264,7 @@ fn get_before_uninstall(kill_self: bool) -> String {
|
||||
}
|
||||
|
||||
fn get_uninstall(kill_self: bool) -> String {
|
||||
let (subkey, path, start_menu, _) = get_install_info();
|
||||
let (subkey, path, start_menu, _, _) = get_install_info();
|
||||
format!(
|
||||
"
|
||||
{before_uninstall}
|
||||
@ -1331,7 +1368,7 @@ pub fn is_installed() -> bool {
|
||||
service::ServiceAccess,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
let (_, _, _, exe) = get_install_info();
|
||||
let (_, _, _, exe, _) = get_install_info();
|
||||
if !std::fs::metadata(exe).is_ok() {
|
||||
return false;
|
||||
}
|
||||
@ -1347,7 +1384,7 @@ pub fn is_installed() -> bool {
|
||||
}
|
||||
|
||||
pub fn get_installed_version() -> String {
|
||||
let (_, _, _, exe) = get_install_info();
|
||||
let (_, _, _, exe, _) = get_install_info();
|
||||
if let Ok(output) = std::process::Command::new(exe).arg("--version").output() {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
return line.to_owned();
|
||||
@ -1357,7 +1394,7 @@ pub fn get_installed_version() -> String {
|
||||
}
|
||||
|
||||
fn get_reg(name: &str) -> String {
|
||||
let (subkey, _, _, _) = get_install_info();
|
||||
let (subkey, _, _, _, _) = get_install_info();
|
||||
get_reg_of(&subkey, name)
|
||||
}
|
||||
|
||||
@ -1408,7 +1445,7 @@ pub fn bootstrap() {
|
||||
}
|
||||
|
||||
fn register_licence() -> String {
|
||||
let (subkey, _, _, _) = get_install_info();
|
||||
let (subkey, _, _, _, _) = get_install_info();
|
||||
if let Ok(lic) = get_license_from_exe_name() {
|
||||
format!(
|
||||
"
|
||||
@ -1765,14 +1802,17 @@ pub fn send_message_to_hnwd(
|
||||
|
||||
pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> {
|
||||
let last_error_table = HashMap::from([
|
||||
(ERROR_LOGON_FAILURE, "The user name or password is incorrect."),
|
||||
(ERROR_ACCESS_DENIED, "Access is denied.")
|
||||
(
|
||||
ERROR_LOGON_FAILURE,
|
||||
"The user name or password is incorrect.",
|
||||
),
|
||||
(ERROR_ACCESS_DENIED, "Access is denied."),
|
||||
]);
|
||||
|
||||
unsafe {
|
||||
let user_split = user.split("\\").collect::<Vec<&str>>();
|
||||
let wuser = wide_string(user_split.get(1).unwrap_or(&user));
|
||||
let wpc = wide_string(user_split.get(0).unwrap_or(&""));
|
||||
let wpc = wide_string(user_split.get(0).unwrap_or(&""));
|
||||
let wpwd = wide_string(pwd);
|
||||
let cmd = if arg.is_empty() {
|
||||
format!("\"{}\"", exe)
|
||||
@ -1804,7 +1844,7 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) ->
|
||||
{
|
||||
let last_error = GetLastError();
|
||||
bail!(
|
||||
"CreateProcessWithLogonW failed : \"{}\", errno={}",
|
||||
"CreateProcessWithLogonW failed : \"{}\", errno={}",
|
||||
last_error_table
|
||||
.get(&last_error)
|
||||
.unwrap_or(&"Unknown error"),
|
||||
@ -2091,7 +2131,25 @@ mod cert {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_char_by_vk(vk: u32) -> Option<char> {
|
||||
#[inline]
|
||||
pub fn get_char_from_vk(vk: u32) -> Option<char> {
|
||||
get_char_from_unicode(get_unicode_from_vk(vk)?)
|
||||
}
|
||||
|
||||
pub fn get_char_from_unicode(unicode: u16) -> Option<char> {
|
||||
let buff = [unicode];
|
||||
if let Some(chr) = String::from_utf16(&buff[..1]).ok()?.chars().next() {
|
||||
if chr.is_control() {
|
||||
return None;
|
||||
} else {
|
||||
Some(chr)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_unicode_from_vk(vk: u32) -> Option<u16> {
|
||||
const BUF_LEN: i32 = 32;
|
||||
let mut buff = [0_u16; BUF_LEN as usize];
|
||||
let buff_ptr = buff.as_mut_ptr();
|
||||
@ -2116,19 +2174,7 @@ pub fn get_char_by_vk(vk: u32) -> Option<char> {
|
||||
ToUnicodeEx(vk, 0x00, &state as _, buff_ptr, BUF_LEN, 0, layout)
|
||||
};
|
||||
if len == 1 {
|
||||
if let Some(chr) = String::from_utf16(&buff[..len as usize])
|
||||
.ok()?
|
||||
.chars()
|
||||
.next()
|
||||
{
|
||||
if chr.is_control() {
|
||||
return None;
|
||||
} else {
|
||||
Some(chr)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(buff[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -2151,10 +2197,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_char_by_vk() {
|
||||
let chr = get_char_by_vk(0x41); // VK_A
|
||||
fn test_get_unicode_char_by_vk() {
|
||||
let chr = get_char_from_vk(0x41); // VK_A
|
||||
assert_eq!(chr, Some('a'));
|
||||
let chr = get_char_by_vk(VK_ESCAPE as u32); // VK_ESC
|
||||
let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC
|
||||
assert_eq!(chr, None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,8 +199,8 @@ async fn connect_and_login_2(
|
||||
},
|
||||
d = ui_receiver.recv() => {
|
||||
match d {
|
||||
Some(Data::Login((password, remember))) => {
|
||||
interface.handle_login_from_ui(password, remember, &mut stream).await;
|
||||
Some(Data::Login((os_username, os_password, password, remember))) => {
|
||||
interface.handle_login_from_ui(os_username, os_password, password, remember, &mut stream).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -72,6 +72,8 @@ impl RendezvousMediator {
|
||||
allow_err!(super::lan::start_listening());
|
||||
});
|
||||
}
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
crate::platform::linux_desktop_manager::start_xdesktop();
|
||||
loop {
|
||||
Config::reset_online();
|
||||
if Config::get_option("stop-service").is_empty() {
|
||||
@ -96,6 +98,10 @@ impl RendezvousMediator {
|
||||
}
|
||||
sleep(1.).await;
|
||||
}
|
||||
// It should be better to call stop_xdesktop.
|
||||
// But for server, it also is Ok without calling this method.
|
||||
// #[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
// crate::platform::linux_desktop_manager::stop_xdesktop();
|
||||
}
|
||||
|
||||
pub async fn start(server: ServerPtr, host: String) -> ResultType<()> {
|
||||
|
||||
@ -389,7 +389,7 @@ pub async fn start_server(is_server: bool) {
|
||||
use std::sync::Once;
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
scrap::hwcodec::check_config_process(false);
|
||||
scrap::hwcodec::check_config_process();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ use super::{input_service::*, *};
|
||||
use crate::clipboard_file::*;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::common::update_clipboard;
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
use crate::platform::linux_desktop_manager;
|
||||
#[cfg(windows)]
|
||||
use crate::portable_service::client as portable_client;
|
||||
use crate::{
|
||||
@ -16,6 +18,8 @@ use crate::{
|
||||
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
|
||||
use crate::{ipc, VERSION};
|
||||
use cidr_utils::cidr::IpCidr;
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
use hbb_common::platform::linux::run_cmds;
|
||||
use hbb_common::{
|
||||
config::Config,
|
||||
fs,
|
||||
@ -45,6 +49,9 @@ use std::{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use system_shutdown;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub type Sender = mpsc::UnboundedSender<(Instant, Arc<Message>)>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@ -56,6 +63,22 @@ lazy_static::lazy_static! {
|
||||
pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0);
|
||||
pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0);
|
||||
|
||||
pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited";
|
||||
pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY: &str = "Desktop session not ready";
|
||||
pub const LOGIN_MSG_DESKTOP_XSESSION_FAILED: &str = "Desktop xsession failed";
|
||||
pub const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER: &str = "Desktop session another user login";
|
||||
pub const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND: &str = "Desktop xorg not found";
|
||||
// ls /usr/share/xsessions/
|
||||
pub const LOGIN_MSG_DESKTOP_NO_DESKTOP: &str = "Desktop none";
|
||||
pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY: &str =
|
||||
"Desktop session not ready, password empty";
|
||||
pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG: &str =
|
||||
"Desktop session not ready, password wrong";
|
||||
pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password";
|
||||
pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password";
|
||||
pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
|
||||
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ConnInner {
|
||||
id: i32,
|
||||
@ -132,6 +155,12 @@ pub struct Connection {
|
||||
voice_call_request_timestamp: Option<NonZeroI64>,
|
||||
audio_input_device_before_voice_call: Option<String>,
|
||||
options_in_login: Option<OptionMessage>,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pressed_modifiers: HashSet<rdev::Key>,
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
rx_cm_stream_ready: mpsc::Receiver<()>,
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
tx_desktop_ready: mpsc::Sender<()>,
|
||||
}
|
||||
|
||||
impl ConnInner {
|
||||
@ -192,6 +221,10 @@ impl Connection {
|
||||
let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc<Message>)>();
|
||||
let (tx_input, _rx_input) = std_mpsc::channel();
|
||||
let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver();
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1);
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1);
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let tx_cloned = tx.clone();
|
||||
@ -243,10 +276,18 @@ impl Connection {
|
||||
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")))]
|
||||
pressed_modifiers: Default::default(),
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
rx_cm_stream_ready: _rx_cm_stream_ready,
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
tx_desktop_ready: _tx_desktop_ready,
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await {
|
||||
if let Err(err) =
|
||||
start_ipc(rx_to_cm, tx_from_cm, rx_desktop_ready, tx_cm_stream_ready).await
|
||||
{
|
||||
log::error!("ipc to connection manager exit: {}", err);
|
||||
}
|
||||
});
|
||||
@ -533,7 +574,7 @@ impl Connection {
|
||||
let _ = privacy_mode::turn_off_privacy(0);
|
||||
}
|
||||
video_service::notify_video_frame_fetched(id, None);
|
||||
scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove);
|
||||
scrap::codec::Encoder::update(id, scrap::codec::EncodingUpdate::Remove);
|
||||
video_service::VIDEO_QOS.lock().unwrap().reset();
|
||||
if conn.authorized {
|
||||
password::update_temporary_password();
|
||||
@ -618,7 +659,6 @@ impl Connection {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
clear_remapped_keycode();
|
||||
release_modifiers();
|
||||
log::info!("Input thread exited");
|
||||
}
|
||||
|
||||
@ -854,22 +894,17 @@ impl Connection {
|
||||
if crate::platform::current_is_wayland() {
|
||||
platform_additions.insert("is_wayland".into(), json!(true));
|
||||
}
|
||||
#[cfg(feature = "linux_headless")]
|
||||
if linux_desktop_manager::is_headless() {
|
||||
platform_additions.insert("headless".into(), json!(true));
|
||||
}
|
||||
if !platform_additions.is_empty() {
|
||||
pi.platform_additions =
|
||||
serde_json::to_string(&platform_additions).unwrap_or("".into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hwcodec")]
|
||||
{
|
||||
let (h264, h265) = scrap::codec::Encoder::supported_encoding();
|
||||
pi.encoding = Some(SupportedEncoding {
|
||||
h264,
|
||||
h265,
|
||||
..Default::default()
|
||||
})
|
||||
.into();
|
||||
}
|
||||
pi.encoding = Some(scrap::codec::Encoder::supported_encoding()).into();
|
||||
|
||||
if self.port_forward_socket.is_some() {
|
||||
let mut msg_out = Message::new();
|
||||
@ -881,7 +916,9 @@ impl Connection {
|
||||
#[cfg(target_os = "linux")]
|
||||
if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() {
|
||||
let dtype = crate::platform::linux::get_display_server();
|
||||
if dtype != "x11" && dtype != "wayland" {
|
||||
if dtype != crate::platform::linux::DISPLAY_SERVER_X11
|
||||
&& dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
res.set_error(format!(
|
||||
"Unsupported display server type \"{}\", x11 or wayland expected",
|
||||
dtype
|
||||
@ -1145,21 +1182,21 @@ impl Connection {
|
||||
self.lr = lr.clone();
|
||||
if let Some(o) = lr.option.as_ref() {
|
||||
self.options_in_login = Some(o.clone());
|
||||
if let Some(q) = o.video_codec_state.clone().take() {
|
||||
scrap::codec::Encoder::update_video_encoder(
|
||||
if let Some(q) = o.supported_decoding.clone().take() {
|
||||
scrap::codec::Encoder::update(
|
||||
self.inner.id(),
|
||||
scrap::codec::EncoderUpdate::State(q),
|
||||
scrap::codec::EncodingUpdate::New(q),
|
||||
);
|
||||
} else {
|
||||
scrap::codec::Encoder::update_video_encoder(
|
||||
scrap::codec::Encoder::update(
|
||||
self.inner.id(),
|
||||
scrap::codec::EncoderUpdate::DisableHwIfNotExist,
|
||||
scrap::codec::EncodingUpdate::NewOnlyVP9,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
scrap::codec::Encoder::update_video_encoder(
|
||||
scrap::codec::Encoder::update(
|
||||
self.inner.id(),
|
||||
scrap::codec::EncoderUpdate::DisableHwIfNotExist,
|
||||
scrap::codec::EncodingUpdate::NewOnlyVP9,
|
||||
);
|
||||
}
|
||||
self.video_ack_required = lr.video_ack_required;
|
||||
@ -1223,8 +1260,28 @@ impl Connection {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
let desktop_err = match lr.os_login.as_ref() {
|
||||
Some(os_login) => {
|
||||
linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password)
|
||||
}
|
||||
None => linux_desktop_manager::try_start_desktop("", ""),
|
||||
};
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
let is_headless = linux_desktop_manager::is_headless();
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
let wait_ipc_timeout = 10_000;
|
||||
|
||||
// If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password.
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
if !desktop_err.is_empty() && desktop_err != LOGIN_MSG_DESKTOP_SESSION_NOT_READY {
|
||||
self.send_login_error(desktop_err).await;
|
||||
return true;
|
||||
}
|
||||
|
||||
if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() {
|
||||
self.send_login_error("Offline").await;
|
||||
self.send_login_error(LOGIN_MSG_OFFLINE).await;
|
||||
} else if password::approve_mode() == ApproveMode::Click
|
||||
|| password::approve_mode() == ApproveMode::Both && !password::has_valid_password()
|
||||
{
|
||||
@ -1232,7 +1289,7 @@ impl Connection {
|
||||
if hbb_common::get_version_number(&lr.version)
|
||||
>= hbb_common::get_version_number("1.2.0")
|
||||
{
|
||||
self.send_login_error("No Password Access").await;
|
||||
self.send_login_error(LOGIN_MSG_NO_PASSWORD_ACCESS).await;
|
||||
}
|
||||
return true;
|
||||
} else if password::approve_mode() == ApproveMode::Password
|
||||
@ -1241,12 +1298,38 @@ impl Connection {
|
||||
self.send_login_error("Connection not allowed").await;
|
||||
return false;
|
||||
} else if self.is_recent_session() {
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true);
|
||||
self.send_logon_response().await;
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
if desktop_err.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
if is_headless {
|
||||
self.tx_desktop_ready.send(()).await.ok();
|
||||
let _res = timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await;
|
||||
}
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true);
|
||||
self.send_logon_response().await;
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
self.send_login_error(desktop_err).await;
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
|
||||
{
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true);
|
||||
self.send_logon_response().await;
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if lr.password.is_empty() {
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
if desktop_err.is_empty() {
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
} else {
|
||||
self.send_login_error(LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY)
|
||||
.await;
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
} else {
|
||||
let mut failure = LOGIN_FAILURES
|
||||
@ -1288,16 +1371,46 @@ impl Connection {
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(self.ip.clone(), failure);
|
||||
self.send_login_error("Wrong Password").await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
if desktop_err.is_empty() {
|
||||
self.send_login_error(LOGIN_MSG_PASSWORD_WRONG).await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
} else {
|
||||
self.send_login_error(LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG)
|
||||
.await;
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
|
||||
{
|
||||
self.send_login_error(LOGIN_MSG_PASSWORD_WRONG).await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, false);
|
||||
}
|
||||
} else {
|
||||
if failure.0 != 0 {
|
||||
LOGIN_FAILURES.lock().unwrap().remove(&self.ip);
|
||||
}
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true);
|
||||
self.send_logon_response().await;
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
if desktop_err.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
if is_headless {
|
||||
self.tx_desktop_ready.send(()).await.ok();
|
||||
let _res =
|
||||
timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await;
|
||||
}
|
||||
self.send_logon_response().await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true);
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
self.send_login_error(desktop_err).await;
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
|
||||
{
|
||||
self.send_logon_response().await;
|
||||
self.try_start_cm(lr.my_id, lr.my_name, true);
|
||||
if self.port_forward_socket.is_some() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1366,6 +1479,30 @@ impl Connection {
|
||||
} else {
|
||||
me.press
|
||||
};
|
||||
|
||||
let key = match me.mode.enum_value_or_default() {
|
||||
KeyboardMode::Map => {
|
||||
Some(crate::keyboard::keycode_to_rdev_key(me.chr()))
|
||||
}
|
||||
KeyboardMode::Translate => {
|
||||
if let Some(key_event::Union::Chr(code)) = me.union {
|
||||
Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.filter(crate::keyboard::is_modifier);
|
||||
|
||||
if let Some(key) = key {
|
||||
if is_press {
|
||||
self.pressed_modifiers.insert(key);
|
||||
} else {
|
||||
self.pressed_modifiers.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
if is_press {
|
||||
match me.union {
|
||||
Some(key_event::Union::Unicode(_))
|
||||
@ -1758,11 +1895,8 @@ impl Connection {
|
||||
.unwrap()
|
||||
.update_user_fps(o.custom_fps as _);
|
||||
}
|
||||
if let Some(q) = o.video_codec_state.clone().take() {
|
||||
scrap::codec::Encoder::update_video_encoder(
|
||||
self.inner.id(),
|
||||
scrap::codec::EncoderUpdate::State(q),
|
||||
);
|
||||
if let Some(q) = o.supported_decoding.clone().take() {
|
||||
scrap::codec::Encoder::update(self.inner.id(), scrap::codec::EncodingUpdate::New(q));
|
||||
}
|
||||
if let Ok(q) = o.lock_after_session_end.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
@ -2023,6 +2157,14 @@ impl Connection {
|
||||
})
|
||||
.count();
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn release_pressed_modifiers(&mut self) {
|
||||
for modifier in self.pressed_modifiers.iter() {
|
||||
rdev::simulate(&rdev::EventType::KeyRelease(*modifier)).ok();
|
||||
}
|
||||
self.pressed_modifiers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
||||
@ -2036,6 +2178,8 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
||||
async fn start_ipc(
|
||||
mut rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
|
||||
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
|
||||
mut _rx_desktop_ready: mpsc::Receiver<()>,
|
||||
tx_stream_ready: mpsc::Sender<()>,
|
||||
) -> ResultType<()> {
|
||||
loop {
|
||||
if !crate::platform::is_prelogin() {
|
||||
@ -2051,6 +2195,34 @@ async fn start_ipc(
|
||||
if password::hide_cm() {
|
||||
args.push("--hide");
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(not(feature = "linux_headless"))]
|
||||
let user = None;
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
let mut user = None;
|
||||
// Cm run as user, wait until desktop session is ready.
|
||||
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
|
||||
if linux_desktop_manager::is_headless() {
|
||||
let mut username = linux_desktop_manager::get_username();
|
||||
loop {
|
||||
if !username.is_empty() {
|
||||
break;
|
||||
}
|
||||
let _res = timeout(1_000, _rx_desktop_ready.recv()).await;
|
||||
username = linux_desktop_manager::get_username();
|
||||
}
|
||||
let uid = {
|
||||
let output = run_cmds(&format!("id -u {}", &username))?;
|
||||
let output = output.trim();
|
||||
if output.is_empty() || !output.parse::<i32>().is_ok() {
|
||||
bail!("Invalid username {}", &username);
|
||||
}
|
||||
output.to_string()
|
||||
};
|
||||
user = Some((uid, username));
|
||||
args = vec!["--cm-no-ui"];
|
||||
}
|
||||
let run_done;
|
||||
if crate::platform::is_root() {
|
||||
let mut res = Ok(None);
|
||||
@ -2063,7 +2235,7 @@ async fn start_ipc(
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
log::debug!("Start cm");
|
||||
res = crate::platform::run_as_user(args.clone(), None);
|
||||
res = crate::platform::run_as_user(args.clone(), user.clone());
|
||||
}
|
||||
if res.is_ok() {
|
||||
break;
|
||||
@ -2095,6 +2267,8 @@ async fn start_ipc(
|
||||
bail!("Failed to connect to connection manager");
|
||||
}
|
||||
}
|
||||
|
||||
let _res = tx_stream_ready.send(()).await;
|
||||
let mut stream = stream.unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
@ -2220,3 +2394,10 @@ impl Default for PortableState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
self.release_pressed_modifiers();
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,9 +16,19 @@ use std::{
|
||||
thread,
|
||||
time::{self, Instant},
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use winapi::um::winuser::{
|
||||
ActivateKeyboardLayout, GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId,
|
||||
VkKeyScanW,
|
||||
};
|
||||
|
||||
const INVALID_CURSOR_POS: i32 = i32::MIN;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref LAST_HKL: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StateCursor {
|
||||
hcursor: u64,
|
||||
@ -551,7 +561,7 @@ fn record_key_to_key(record_key: u64) -> Option<Key> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_modifiers() {
|
||||
pub fn release_device_modifiers() {
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
for modifier in [
|
||||
Key::Shift,
|
||||
@ -1242,7 +1252,45 @@ fn translate_process_code(code: u32, down: bool) {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn check_update_input_layout() {
|
||||
unsafe {
|
||||
let foreground_thread_id =
|
||||
GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut());
|
||||
let layout = GetKeyboardLayout(foreground_thread_id);
|
||||
let layout_u32 = layout as u32;
|
||||
let mut last_layout_lock = LAST_HKL.lock().unwrap();
|
||||
if *last_layout_lock == 0 || *last_layout_lock != layout_u32 {
|
||||
let res = ActivateKeyboardLayout(layout, 0);
|
||||
if res == layout {
|
||||
*last_layout_lock = layout_u32;
|
||||
} else {
|
||||
log::error!("Failed to call ActivateKeyboardLayout, {}", layout_u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_keyboard_mode(evt: &KeyEvent) {
|
||||
// --server could not detect the input layout change.
|
||||
// This is a temporary workaround.
|
||||
//
|
||||
// There may be a better way to detect and handle the input layout change.
|
||||
// while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
|
||||
// {
|
||||
// ...
|
||||
// if (msg.message == WM_INPUTLANGCHANGE)
|
||||
// {
|
||||
// // handle WM_INPUTLANGCHANGE message here
|
||||
// check_update_input_layout();
|
||||
// }
|
||||
// TranslateMessage(&msg);
|
||||
// DispatchMessage(&msg);
|
||||
// ...
|
||||
// }
|
||||
#[cfg(target_os = "windows")]
|
||||
check_update_input_layout();
|
||||
|
||||
match &evt.union {
|
||||
Some(key_event::Union::Seq(seq)) => {
|
||||
// Fr -> US
|
||||
@ -1280,28 +1328,117 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
|
||||
Some(key_event::Union::Unicode(..)) => {
|
||||
// Do not handle unicode for now.
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
Some(key_event::Union::Win2winHotkey(code)) => {
|
||||
simulate_win2win_hotkey(*code, evt.down);
|
||||
}
|
||||
_ => {
|
||||
log::debug!("Unreachable. Unexpected key event {:?}", &evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn simulate_win2win_hotkey(code: u32, down: bool) {
|
||||
let unicode: u16 = (code & 0x0000FFFF) as u16;
|
||||
if down {
|
||||
// Try convert unicode to virtual keycode first.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
|
||||
let res = unsafe { VkKeyScanW(unicode) };
|
||||
if res as u16 != 0xFFFF {
|
||||
let vk = res & 0x00FF;
|
||||
let flag = res >> 8;
|
||||
let modifiers = [rdev::Key::ShiftLeft, rdev::Key::ControlLeft, rdev::Key::Alt];
|
||||
let mod_len = modifiers.len();
|
||||
for pos in 0..mod_len {
|
||||
if flag & (0x0001 << pos) != 0 {
|
||||
allow_err!(rdev::simulate(&EventType::KeyPress(modifiers[pos])));
|
||||
}
|
||||
}
|
||||
allow_err!(rdev::simulate_code(Some(vk as _), None, true));
|
||||
allow_err!(rdev::simulate_code(Some(vk as _), None, false));
|
||||
for pos in 0..mod_len {
|
||||
let rpos = mod_len - 1 - pos;
|
||||
if flag & (0x0001 << rpos) != 0 {
|
||||
allow_err!(rdev::simulate(&EventType::KeyRelease(modifiers[rpos])));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let keycode: u16 = ((code >> 16) & 0x0000FFFF) as u16;
|
||||
allow_err!(rdev::simulate_code(Some(keycode), None, down));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
fn skip_led_sync_control_key(_evt: &KeyEvent) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// LockModesHandler should not be created when single meta is pressing and releasing.
|
||||
// Because the drop function may insert "CapsLock Click" and "NumLock Click", which breaks single meta click.
|
||||
// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1496936687
|
||||
// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500415822
|
||||
// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500773473
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
fn skip_led_sync_control_key(key: &ControlKey) -> bool {
|
||||
[
|
||||
ControlKey::Control,
|
||||
ControlKey::Meta,
|
||||
ControlKey::Shift,
|
||||
ControlKey::Alt,
|
||||
ControlKey::Tab,
|
||||
ControlKey::Return,
|
||||
]
|
||||
.contains(key)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
fn skip_led_sync_rdev_key(_evt: &KeyEvent) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
fn skip_led_sync_rdev_key(key: &RdevKey) -> bool {
|
||||
[
|
||||
RdevKey::ControlLeft,
|
||||
RdevKey::ControlRight,
|
||||
RdevKey::MetaLeft,
|
||||
RdevKey::MetaRight,
|
||||
RdevKey::ShiftRight,
|
||||
RdevKey::ShiftRight,
|
||||
RdevKey::Alt,
|
||||
RdevKey::AltGr,
|
||||
RdevKey::Tab,
|
||||
RdevKey::Return,
|
||||
]
|
||||
.contains(key)
|
||||
}
|
||||
|
||||
pub fn handle_key_(evt: &KeyEvent) {
|
||||
if EXITING.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _lock_mode_handler = match &evt.union {
|
||||
Some(key_event::Union::Unicode(..)) | Some(key_event::Union::Seq(..)) => {
|
||||
Some(LockModesHandler::new(&evt))
|
||||
let mut _lock_mode_handler = None;
|
||||
match (&evt.union, evt.mode.enum_value_or(KeyboardMode::Legacy)) {
|
||||
(Some(key_event::Union::Unicode(..)) | Some(key_event::Union::Seq(..)), _) => {
|
||||
_lock_mode_handler = Some(LockModesHandler::new(&evt));
|
||||
}
|
||||
_ => {
|
||||
if evt.down {
|
||||
Some(LockModesHandler::new(&evt))
|
||||
} else {
|
||||
None
|
||||
(Some(key_event::Union::ControlKey(ck)), _) => {
|
||||
let key = ck.enum_value_or(ControlKey::Unknown);
|
||||
if !skip_led_sync_control_key(&key) {
|
||||
_lock_mode_handler = Some(LockModesHandler::new(&evt));
|
||||
}
|
||||
}
|
||||
(Some(key_event::Union::Chr(code)), KeyboardMode::Map | KeyboardMode::Translate) => {
|
||||
let key = crate::keycode_to_rdev_key(*code);
|
||||
if !skip_led_sync_rdev_key(&key) {
|
||||
_lock_mode_handler = Some(LockModesHandler::new(evt));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
match evt.mode.unwrap() {
|
||||
|
||||
@ -189,17 +189,6 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wait_prelogin(&self) {
|
||||
#[cfg(target_os = "linux")]
|
||||
while self.active() {
|
||||
if crate::platform::linux::is_prelogin() {
|
||||
break;
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(300));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repeat<S, F>(&self, interval_ms: u64, callback: F)
|
||||
where
|
||||
F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send,
|
||||
@ -209,8 +198,6 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
let mut callback = callback;
|
||||
let sp = self.clone();
|
||||
let thread = thread::spawn(move || {
|
||||
sp.wait_prelogin();
|
||||
|
||||
let mut state = S::default();
|
||||
let mut may_reset = false;
|
||||
while sp.active() {
|
||||
@ -234,6 +221,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
thread::sleep(interval - elapsed);
|
||||
}
|
||||
}
|
||||
log::info!("Service {} exit", sp.name());
|
||||
});
|
||||
self.0.write().unwrap().handle = Some(thread);
|
||||
}
|
||||
@ -245,8 +233,6 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
let sp = self.clone();
|
||||
let mut callback = callback;
|
||||
let thread = thread::spawn(move || {
|
||||
sp.wait_prelogin();
|
||||
|
||||
let mut error_timeout = HIBERNATE_TIMEOUT;
|
||||
while sp.active() {
|
||||
if sp.has_subscribes() {
|
||||
@ -271,6 +257,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT));
|
||||
}
|
||||
log::info!("Service {} exit", sp.name());
|
||||
});
|
||||
self.0.write().unwrap().handle = Some(thread);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
pub const FPS: u8 = 30;
|
||||
pub const MIN_FPS: u8 = 10;
|
||||
pub const MIN_FPS: u8 = 1;
|
||||
pub const MAX_FPS: u8 = 120;
|
||||
trait Percent {
|
||||
fn as_percent(&self) -> u32;
|
||||
@ -221,7 +221,9 @@ impl VideoQoS {
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
*self = Default::default();
|
||||
self.fps = FPS;
|
||||
self.user_fps = FPS;
|
||||
self.updated = true;
|
||||
}
|
||||
|
||||
pub fn check_abr_config(&mut self) -> bool {
|
||||
|
||||
@ -31,7 +31,7 @@ use scrap::{
|
||||
codec::{Encoder, EncoderCfg, HwEncoderConfig},
|
||||
record::{Recorder, RecorderContext},
|
||||
vpxcodec::{VpxEncoderConfig, VpxVideoCodecId},
|
||||
Display, TraitCapturer,
|
||||
CodecName, Display, TraitCapturer,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use std::sync::Once;
|
||||
@ -468,21 +468,29 @@ fn run(sp: GenericService) -> ResultType<()> {
|
||||
drop(video_qos);
|
||||
log::info!("init bitrate={}, abr enabled:{}", bitrate, abr);
|
||||
|
||||
let encoder_cfg = match Encoder::current_hw_encoder_name() {
|
||||
Some(codec_name) => EncoderCfg::HW(HwEncoderConfig {
|
||||
codec_name,
|
||||
width: c.width,
|
||||
height: c.height,
|
||||
bitrate: bitrate as _,
|
||||
}),
|
||||
None => EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: c.width as _,
|
||||
height: c.height as _,
|
||||
timebase: [1, 1000], // Output timestamp precision
|
||||
bitrate,
|
||||
codec: VpxVideoCodecId::VP9,
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
}),
|
||||
let encoder_cfg = match Encoder::negotiated_codec() {
|
||||
scrap::CodecName::H264(name) | scrap::CodecName::H265(name) => {
|
||||
EncoderCfg::HW(HwEncoderConfig {
|
||||
name,
|
||||
width: c.width,
|
||||
height: c.height,
|
||||
bitrate: bitrate as _,
|
||||
})
|
||||
}
|
||||
name @ (scrap::CodecName::VP8 | scrap::CodecName::VP9) => {
|
||||
EncoderCfg::VPX(VpxEncoderConfig {
|
||||
width: c.width as _,
|
||||
height: c.height as _,
|
||||
timebase: [1, 1000], // Output timestamp precision
|
||||
bitrate,
|
||||
codec: if name == scrap::CodecName::VP8 {
|
||||
VpxVideoCodecId::VP8
|
||||
} else {
|
||||
VpxVideoCodecId::VP9
|
||||
},
|
||||
num_threads: (num_cpus::get() / 2) as _,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let mut encoder;
|
||||
@ -526,7 +534,7 @@ fn run(sp: GenericService) -> ResultType<()> {
|
||||
let mut try_gdi = 1;
|
||||
#[cfg(windows)]
|
||||
log::info!("gdi: {}", c.is_gdi());
|
||||
let codec_name = Encoder::current_hw_encoder_name();
|
||||
let codec_name = Encoder::negotiated_codec();
|
||||
let recorder = get_recorder(c.width, c.height, &codec_name);
|
||||
#[cfg(windows)]
|
||||
start_uac_elevation_check();
|
||||
@ -539,7 +547,7 @@ fn run(sp: GenericService) -> ResultType<()> {
|
||||
check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?;
|
||||
|
||||
let mut video_qos = VIDEO_QOS.lock().unwrap();
|
||||
if video_qos.check_if_updated() {
|
||||
if video_qos.check_if_updated() && video_qos.target_bitrate > 0 {
|
||||
log::debug!(
|
||||
"qos is updated, target_bitrate:{}, fps:{}",
|
||||
video_qos.target_bitrate,
|
||||
@ -557,7 +565,7 @@ fn run(sp: GenericService) -> ResultType<()> {
|
||||
*SWITCH.lock().unwrap() = true;
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if codec_name != Encoder::current_hw_encoder_name() {
|
||||
if codec_name != Encoder::negotiated_codec() {
|
||||
bail!("SWITCH");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
@ -603,8 +611,14 @@ fn run(sp: GenericService) -> ResultType<()> {
|
||||
let time = now - start;
|
||||
let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64;
|
||||
match frame {
|
||||
scrap::Frame::VP8(data) => {
|
||||
let send_conn_ids =
|
||||
handle_one_frame_encoded(VpxVideoCodecId::VP8, &sp, data, ms)?;
|
||||
frame_controller.set_send(now, send_conn_ids);
|
||||
}
|
||||
scrap::Frame::VP9(data) => {
|
||||
let send_conn_ids = handle_one_frame_encoded(&sp, data, ms)?;
|
||||
let send_conn_ids =
|
||||
handle_one_frame_encoded(VpxVideoCodecId::VP9, &sp, data, ms)?;
|
||||
frame_controller.set_send(now, send_conn_ids);
|
||||
}
|
||||
scrap::Frame::RAW(data) => {
|
||||
@ -717,12 +731,11 @@ fn run(sp: GenericService) -> ResultType<()> {
|
||||
fn get_recorder(
|
||||
width: usize,
|
||||
height: usize,
|
||||
codec_name: &Option<String>,
|
||||
codec_name: &CodecName,
|
||||
) -> Arc<Mutex<Option<Recorder>>> {
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() {
|
||||
use crate::hbbs_http::record_upload;
|
||||
use scrap::record::RecordCodecID::*;
|
||||
|
||||
let tx = if record_upload::is_enable() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
@ -731,16 +744,6 @@ fn get_recorder(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let codec_id = match codec_name {
|
||||
Some(name) => {
|
||||
if name.contains("264") {
|
||||
H264
|
||||
} else {
|
||||
H265
|
||||
}
|
||||
}
|
||||
None => VP9,
|
||||
};
|
||||
Recorder::new(RecorderContext {
|
||||
server: true,
|
||||
id: Config::get_id(),
|
||||
@ -748,7 +751,7 @@ fn get_recorder(
|
||||
filename: "".to_owned(),
|
||||
width,
|
||||
height,
|
||||
codec_id,
|
||||
format: codec_name.into(),
|
||||
tx,
|
||||
})
|
||||
.map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r))))
|
||||
@ -775,19 +778,6 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
fn create_msg(vp9s: Vec<EncodedVideoFrame>) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut vf = VideoFrame::new();
|
||||
vf.set_vp9s(EncodedVideoFrames {
|
||||
frames: vp9s.into(),
|
||||
..Default::default()
|
||||
});
|
||||
msg_out.set_video_frame(vf);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn handle_one_frame(
|
||||
sp: &GenericService,
|
||||
@ -820,6 +810,7 @@ fn handle_one_frame(
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub fn handle_one_frame_encoded(
|
||||
codec: VpxVideoCodecId,
|
||||
sp: &GenericService,
|
||||
frame: &[u8],
|
||||
ms: i64,
|
||||
@ -831,13 +822,13 @@ pub fn handle_one_frame_encoded(
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let vp9_frame = EncodedVideoFrame {
|
||||
let vpx_frame = EncodedVideoFrame {
|
||||
data: frame.to_vec().into(),
|
||||
key: true,
|
||||
pts: ms,
|
||||
..Default::default()
|
||||
};
|
||||
let send_conn_ids = sp.send_video_frame(create_msg(vec![vp9_frame]));
|
||||
let send_conn_ids = sp.send_video_frame(scrap::VpxEncoder::create_msg(codec, vec![vpx_frame]));
|
||||
Ok(send_conn_ids)
|
||||
}
|
||||
|
||||
|
||||
@ -142,6 +142,10 @@ div.password input {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.username input {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
svg {
|
||||
background: none;
|
||||
}
|
||||
|
||||
@ -252,7 +252,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width=
|
||||
view.close();
|
||||
return;
|
||||
}
|
||||
handler.login(res.password, res.remember);
|
||||
handler.login("", "", res.password, res.remember);
|
||||
if (!is_port_forward) {
|
||||
// Specially handling file transfer for no permission hanging issue (including 60ms
|
||||
// timer in setPermission.
|
||||
@ -262,6 +262,30 @@ function msgbox(type, title, content, link="", callback=null, height=180, width=
|
||||
else msgbox("connecting", "Connecting...", "Logging in...");
|
||||
}
|
||||
};
|
||||
} else if (type == "session-login" || type == "session-re-login") {
|
||||
callback = function (res) {
|
||||
if (!res) {
|
||||
view.close();
|
||||
return;
|
||||
}
|
||||
handler.login(res.osusername, res.ospassword, "", false);
|
||||
if (!is_port_forward) {
|
||||
if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in...");
|
||||
else msgbox("connecting", "Connecting...", "Logging in...");
|
||||
}
|
||||
};
|
||||
} else if (type.indexOf("session-login") >= 0) {
|
||||
callback = function (res) {
|
||||
if (!res) {
|
||||
view.close();
|
||||
return;
|
||||
}
|
||||
handler.login(res.osusername, res.ospassword, res.password, res.remember);
|
||||
if (!is_port_forward) {
|
||||
if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in...");
|
||||
else msgbox("connecting", "Connecting...", "Logging in...");
|
||||
}
|
||||
};
|
||||
} else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) {
|
||||
callback = function() { view.close(); }
|
||||
} else if (type == 'wait-remote-accept-nook') {
|
||||
|
||||
@ -161,8 +161,8 @@ class Header: Reactor.Component {
|
||||
}
|
||||
|
||||
function renderDisplayPop() {
|
||||
var codecs = handler.supported_hwcodec();
|
||||
var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]);
|
||||
var codecs = handler.alternative_codecs();
|
||||
var show_codec = codecs[0] || codecs[1] || codecs[2];
|
||||
|
||||
var cursor_embedded = false;
|
||||
if ((pi.displays || []).length > 0) {
|
||||
@ -186,9 +186,10 @@ class Header: Reactor.Component {
|
||||
{show_codec ? <div>
|
||||
<div .separator />
|
||||
<li #auto type="codec-preference"><span>{svg_checkmark}</span>Auto</li>
|
||||
{codecs[0] ? <li #vp8 type="codec-preference"><span>{svg_checkmark}</span>VP8</li> : ""}
|
||||
<li #vp9 type="codec-preference"><span>{svg_checkmark}</span>VP9</li>
|
||||
{codecs[0] ? <li #h264 type="codec-preference"><span>{svg_checkmark}</span>H264</li> : ""}
|
||||
{codecs[1] ? <li #h265 type="codec-preference"><span>{svg_checkmark}</span>H265</li> : ""}
|
||||
{codecs[1] ? <li #h264 type="codec-preference"><span>{svg_checkmark}</span>H264</li> : ""}
|
||||
{codecs[2] ? <li #h265 type="codec-preference"><span>{svg_checkmark}</span>H265</li> : ""}
|
||||
</div> : ""}
|
||||
<div .separator />
|
||||
{!cursor_embedded && <li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>{translate('Show remote cursor')}</li>}
|
||||
|
||||
@ -32,7 +32,7 @@ class MsgboxComponent: Reactor.Component {
|
||||
}
|
||||
|
||||
function getIcon(color) {
|
||||
if (this.type == "input-password") {
|
||||
if (this.type == "input-password" || this.type == "session-login" || this.type == "session-login-password") {
|
||||
return <svg viewBox="0 0 505 505"><circle cx="252.5" cy="252.5" r="252.5" fill={color}/><path d="M271.9 246.1c29.2 17.5 67.6 13.6 92.7-11.5 29.7-29.7 29.7-77.8 0-107.4s-77.8-29.7-107.4 0c-25.1 25.1-29 63.5-11.5 92.7L118.1 347.4l26.2 26.2 26.4 26.4 10.6-10.6-10.1-10.1 9.7-9.7 10.1 10.1 10.6-10.6-10.1-10 9.7-9.7 10.1 10.1 10.6-10.6-26.4-26.3 76.4-76.5z" fill="#fff"/><circle cx="337.4" cy="154.4" r="17.7" fill={color}/></svg>;
|
||||
}
|
||||
if (this.type == "connecting") {
|
||||
@ -41,7 +41,7 @@ class MsgboxComponent: Reactor.Component {
|
||||
if (this.type == "success") {
|
||||
return <svg viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill={color} /><path fill="#fff" d="M235.472 392.08l-121.04-94.296 34.416-44.168 74.328 57.904 122.672-177.016 46.032 31.888z"/></svg>;
|
||||
}
|
||||
if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") {
|
||||
if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") {
|
||||
return <svg viewBox="0 0 512 512"><ellipse cx="256" cy="256" rx="256" ry="255.832" fill={color}/><g fill="#fff"><path d="M376.812 337.18l-39.592 39.593-201.998-201.999 39.592-39.592z"/><path d="M376.818 174.825L174.819 376.824l-39.592-39.592 201.999-201.999z"/></g></svg>;
|
||||
}
|
||||
return null;
|
||||
@ -56,11 +56,36 @@ class MsgboxComponent: Reactor.Component {
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getInputUserPasswordContent() {
|
||||
return <div .form>
|
||||
<div>{translate("OS Username")}</div>
|
||||
<div .username><input name='osusername' type='text' .outline-focus /></div>
|
||||
<div>{translate("OS Password")}</div>
|
||||
<PasswordComponent name='ospassword' />
|
||||
<div></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getXsessionPasswordContent() {
|
||||
return <div .form>
|
||||
<div>{translate("OS Username")}</div>
|
||||
<div .username><input name='osusername' type='text' .outline-focus /></div>
|
||||
<div>{translate("OS Password")}</div>
|
||||
<PasswordComponent name='ospassword' />
|
||||
<div>{translate('Please enter your password')}</div>
|
||||
<PasswordComponent />
|
||||
<div><button|checkbox(remember) {ts}>{translate('Remember password')}</button></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
if (this.type == "input-password") {
|
||||
return this.getInputPasswordContent();
|
||||
}
|
||||
if (this.type == "custom-os-password") {
|
||||
} else if (this.type == "session-login") {
|
||||
return this.getInputUserPasswordContent();
|
||||
} else if (this.type == "session-login-password") {
|
||||
return this.getXsessionPasswordContent();
|
||||
} else if (this.type == "custom-os-password") {
|
||||
var ts = this.auto_login ? { checked: true } : {};
|
||||
return <div .form>
|
||||
<PasswordComponent value={this.content} />
|
||||
@ -71,13 +96,13 @@ class MsgboxComponent: Reactor.Component {
|
||||
}
|
||||
|
||||
function getColor() {
|
||||
if (this.type == "input-password" || this.type == "custom-os-password") {
|
||||
if (this.type == "input-password" || this.type == "custom-os-password" || this.type == "session-login" || this.type == "session-login-password") {
|
||||
return "#AD448E";
|
||||
}
|
||||
if (this.type == "success") {
|
||||
return "#32bea6";
|
||||
}
|
||||
if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") {
|
||||
if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") {
|
||||
return "#e04f5f";
|
||||
}
|
||||
return "#2C8CFF";
|
||||
@ -177,6 +202,16 @@ class MsgboxComponent: Reactor.Component {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
if (this.type == "session-re-login") {
|
||||
this.type = "session-login";
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
if (this.type == "session-login-re-password") {
|
||||
this.type = "session-login-password";
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
var values = this.getValues();
|
||||
if (this.callback) {
|
||||
var self = this;
|
||||
@ -238,6 +273,21 @@ class MsgboxComponent: Reactor.Component {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.type == "session-login") {
|
||||
values.osusername = (values.osusername || "").trim();
|
||||
values.ospassword = (values.ospassword || "").trim();
|
||||
if (!values.osusername || !values.ospassword) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.type == "session-login-password") {
|
||||
values.password = (values.password || "").trim();
|
||||
values.osusername = (values.osusername || "").trim();
|
||||
values.ospassword = (values.ospassword || "").trim();
|
||||
if (!values.osusername || !values.ospassword || !values.password) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ use hbb_common::{
|
||||
|
||||
use crate::{
|
||||
client::*,
|
||||
ui_interface::has_hwcodec,
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
|
||||
@ -197,7 +196,14 @@ impl InvokeUiSession for SciterHandler {
|
||||
self.call("confirmDeleteFiles", &make_args!(id, i, name));
|
||||
}
|
||||
|
||||
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool, is_identical: bool) {
|
||||
fn override_file_confirm(
|
||||
&self,
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
to: String,
|
||||
is_upload: bool,
|
||||
is_identical: bool,
|
||||
) {
|
||||
self.call(
|
||||
"overrideFileConfirm",
|
||||
&make_args!(id, file_num, to, is_upload, is_identical),
|
||||
@ -398,7 +404,7 @@ impl sciter::EventHandler for SciterSession {
|
||||
fn is_file_transfer();
|
||||
fn is_port_forward();
|
||||
fn is_rdp();
|
||||
fn login(String, bool);
|
||||
fn login(String, String, String, bool);
|
||||
fn new_rdp();
|
||||
fn send_mouse(i32, i32, i32, bool, bool, bool, bool);
|
||||
fn enter();
|
||||
@ -451,8 +457,7 @@ impl sciter::EventHandler for SciterSession {
|
||||
fn set_write_override(i32, i32, bool, bool, bool);
|
||||
fn get_keyboard_mode();
|
||||
fn save_keyboard_mode(String);
|
||||
fn has_hwcodec();
|
||||
fn supported_hwcodec();
|
||||
fn alternative_codecs();
|
||||
fn change_prefer_codec();
|
||||
fn restart_remote_device();
|
||||
fn request_voice_call();
|
||||
@ -504,10 +509,6 @@ impl SciterSession {
|
||||
v
|
||||
}
|
||||
|
||||
fn has_hwcodec(&self) -> bool {
|
||||
has_hwcodec()
|
||||
}
|
||||
|
||||
pub fn t(&self, name: String) -> String {
|
||||
crate::client::translate(name)
|
||||
}
|
||||
@ -516,9 +517,10 @@ impl SciterSession {
|
||||
super::get_icon()
|
||||
}
|
||||
|
||||
fn supported_hwcodec(&self) -> Value {
|
||||
let (h264, h265) = self.0.supported_hwcodec();
|
||||
fn alternative_codecs(&self) -> Value {
|
||||
let (vp8, h264, h265) = self.0.alternative_codecs();
|
||||
let mut v = Value::array(0);
|
||||
v.push(vp8);
|
||||
v.push(h264);
|
||||
v.push(h265);
|
||||
v
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user