mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-10 19:00:53 +08:00
terminal works basically. (#12189)
* terminal works basically. todo: - persistent - sessions restore - web - mobile * missed terminal persistent option change * android sdk 34 -> 35 * +#![cfg_attr(lt_1_77, feature(c_str_literals))] * fixing ci * fix ci * fix ci for android * try "Fix Android SDK Platform 35" * fix android 34 * revert flutter_plugin_android_lifecycle to 2.0.17 which used in rustdesk 1.4.0 * refactor, but break something of desktop terminal (new tab showing loading) * fix connecting...
This commit is contained in:
1 parent
ee5cdc3155
commit
5faf0ad3cf
130 files changed
+4064
-4247
No files matched your search
@@ -40,9 +40,9 @@ jobs:
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libclang-11-dev \
|
||||
libclang-dev \
|
||||
libgtk-3-dev \
|
||||
llvm-11-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
|
||||
@@ -929,21 +929,21 @@ jobs:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-linux-android,
|
||||
os: ubuntu-22.04,
|
||||
os: ubuntu-24.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
arch: armv7,
|
||||
target: armv7-linux-androideabi,
|
||||
os: ubuntu-22.04,
|
||||
os: ubuntu-24.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-linux-android,
|
||||
os: ubuntu-22.04,
|
||||
os: ubuntu-24.04,
|
||||
reltype: release,
|
||||
suffix: "",
|
||||
}
|
||||
@@ -980,7 +980,7 @@ jobs:
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-11-dev \
|
||||
libclang-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
@@ -993,7 +993,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-11-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-17-jdk-headless \
|
||||
@@ -1212,7 +1212,7 @@ jobs:
|
||||
needs: [build-rustdesk-android]
|
||||
name: build rustdesk android universal apk
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
reltype: release
|
||||
x86_target: "" # can be ",android-x86"
|
||||
@@ -1250,7 +1250,7 @@ jobs:
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-11-dev \
|
||||
libclang-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
@@ -1263,7 +1263,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-11-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
openjdk-17-jdk-headless \
|
||||
|
||||
@@ -266,7 +266,7 @@ jobs:
|
||||
libayatana-appindicator3-dev\
|
||||
libasound2-dev \
|
||||
libc6-dev \
|
||||
libclang-11-dev \
|
||||
libclang-dev \
|
||||
libunwind-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-11-dev \
|
||||
llvm-dev \
|
||||
nasm \
|
||||
yasm \
|
||||
ninja-build \
|
||||
|
||||
Generated
+137
-5
@@ -2216,6 +2216,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.61",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.23"
|
||||
@@ -3511,6 +3522,15 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ioctl-rs"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.9.0"
|
||||
@@ -3948,7 +3968,7 @@ source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9d
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
"winreg",
|
||||
"winreg 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4286,6 +4306,20 @@ dependencies = [
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
|
||||
dependencies = [
|
||||
"autocfg 1.3.0",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
@@ -5196,6 +5230,27 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"downcast-rs",
|
||||
"filedescriptor",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.25.1",
|
||||
"serial",
|
||||
"shared_library",
|
||||
"shell-words",
|
||||
"winapi 0.3.9",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@@ -5391,7 +5446,7 @@ dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"rpassword 2.1.0",
|
||||
"tempfile",
|
||||
"termios",
|
||||
"termios 0.3.3",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@@ -6067,6 +6122,7 @@ dependencies = [
|
||||
"pam",
|
||||
"parity-tokio-ipc",
|
||||
"percent-encoding",
|
||||
"portable-pty",
|
||||
"qrcode-generator",
|
||||
"rdev",
|
||||
"remote_printer",
|
||||
@@ -6092,7 +6148,7 @@ dependencies = [
|
||||
"system_shutdown",
|
||||
"tao",
|
||||
"tauri-winrt-notification",
|
||||
"termios",
|
||||
"termios 0.3.3",
|
||||
"totp-rs",
|
||||
"tray-icon",
|
||||
"url",
|
||||
@@ -6103,7 +6159,7 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
"windows 0.61.1",
|
||||
"windows-service",
|
||||
"winreg",
|
||||
"winreg 0.11.0",
|
||||
"winres",
|
||||
"wol-rs",
|
||||
"x11-clipboard 0.8.1",
|
||||
@@ -6466,6 +6522,48 @@ dependencies = [
|
||||
"serde 1.0.203",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
|
||||
dependencies = [
|
||||
"serial-core",
|
||||
"serial-unix",
|
||||
"serial-windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial-core"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial-unix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
|
||||
dependencies = [
|
||||
"ioctl-rs",
|
||||
"libc",
|
||||
"serial-core",
|
||||
"termios 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial-windows"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"serial-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -6510,6 +6608,16 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_memory"
|
||||
version = "0.12.4"
|
||||
@@ -6523,6 +6631,12 @@ dependencies = [
|
||||
"win-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -6957,6 +7071,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termios"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termios"
|
||||
version = "0.3.3"
|
||||
@@ -7779,7 +7902,7 @@ dependencies = [
|
||||
"rust-ini",
|
||||
"thiserror 1.0.61",
|
||||
"winapi 0.3.9",
|
||||
"winreg",
|
||||
"winreg 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8758,6 +8881,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.11.0"
|
||||
|
||||
+1
-1
@@ -83,7 +83,6 @@ shutdown_hooks = "0.1"
|
||||
totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] }
|
||||
stunclient = "0.4"
|
||||
kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"}
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux
|
||||
cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" }
|
||||
@@ -99,6 +98,7 @@ ctrlc = "3.2"
|
||||
# arboard = { version = "3.4", features = ["wayland-data-control"] }
|
||||
arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] }
|
||||
clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" }
|
||||
portable-pty = "0.8.1" # higher version not work on rustc 1.75
|
||||
|
||||
system_shutdown = "4.0"
|
||||
qrcode-generator = "4.1"
|
||||
|
||||
@@ -122,9 +122,9 @@ class MainService : Service() {
|
||||
val authorized = jsonObject["authorized"] as Boolean
|
||||
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||
val type = if (isFileTransfer) {
|
||||
translate("File Connection")
|
||||
translate("Transfer file")
|
||||
} else {
|
||||
translate("Screen Connection")
|
||||
translate("Share screen")
|
||||
}
|
||||
if (authorized) {
|
||||
if (!isFileTransfer && !isStart) {
|
||||
|
||||
+72
-25
@@ -30,6 +30,7 @@ import 'common/widgets/overlay.dart';
|
||||
import 'mobile/pages/file_manager_page.dart';
|
||||
import 'mobile/pages/remote_page.dart';
|
||||
import 'mobile/pages/view_camera_page.dart';
|
||||
import 'mobile/pages/terminal_page.dart';
|
||||
import 'desktop/pages/remote_page.dart' as desktop_remote;
|
||||
import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
|
||||
import 'desktop/pages/view_camera_page.dart' as desktop_view_camera;
|
||||
@@ -99,6 +100,7 @@ enum DesktopType {
|
||||
remote,
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
terminal,
|
||||
cm,
|
||||
portForward,
|
||||
}
|
||||
@@ -1571,7 +1573,9 @@ bool option2bool(String option, String value) {
|
||||
|
||||
String bool2option(String option, bool b) {
|
||||
String res;
|
||||
if (option.startsWith('enable-') && option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) {
|
||||
if (option.startsWith('enable-') &&
|
||||
option != kOptionEnableUdpPunch &&
|
||||
option != kOptionEnableIpv6Punch) {
|
||||
res = b ? defaultOptionYes : 'N';
|
||||
} else if (option.startsWith('allow-') ||
|
||||
option == kOptionStopService ||
|
||||
@@ -2117,6 +2121,7 @@ enum UriLinkType {
|
||||
viewCamera,
|
||||
portForward,
|
||||
rdp,
|
||||
terminal,
|
||||
}
|
||||
|
||||
// uri link handler
|
||||
@@ -2181,6 +2186,11 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--terminal':
|
||||
type = UriLinkType.terminal;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
@@ -2230,6 +2240,12 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.terminal:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newTerminal(id!,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -2247,7 +2263,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
"file-transfer",
|
||||
"view-camera",
|
||||
"port-forward",
|
||||
"rdp"
|
||||
"rdp",
|
||||
"terminal"
|
||||
];
|
||||
if (uri.authority.isEmpty &&
|
||||
uri.path.split('').every((char) => char == '/')) {
|
||||
@@ -2276,21 +2293,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
}
|
||||
}
|
||||
} else if (options.contains(uri.authority)) {
|
||||
final optionIndex = options.indexOf(uri.authority);
|
||||
command = '--${uri.authority}';
|
||||
if (uri.path.length > 1) {
|
||||
id = uri.path.substring(1);
|
||||
}
|
||||
if (isMobile && id != null) {
|
||||
if (optionIndex == 0 || optionIndex == 1) {
|
||||
connect(Get.context!, id);
|
||||
} else if (optionIndex == 2) {
|
||||
connect(Get.context!, id, isFileTransfer: true);
|
||||
} else if (optionIndex == 3) {
|
||||
connect(Get.context!, id, isViewCamera: true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else if (uri.authority.length > 2 &&
|
||||
(uri.path.length <= 1 ||
|
||||
(uri.path == '/r' || uri.path.startsWith('/r@')))) {
|
||||
@@ -2314,13 +2320,25 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
if (id != null) {
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
if (isMobile && id != null) {
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
final password = queryParameters["password"];
|
||||
|
||||
// Determine connection type based on command
|
||||
if (command == '--file-transfer') {
|
||||
connect(Get.context!, id,
|
||||
forceRelay: forceRelay, password: queryParameters["password"]);
|
||||
return null;
|
||||
isFileTransfer: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == '--view-camera') {
|
||||
connect(Get.context!, id,
|
||||
isViewCamera: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == '--terminal') {
|
||||
connect(Get.context!, id,
|
||||
isTerminal: true, forceRelay: forceRelay, password: password);
|
||||
} else {
|
||||
// Default to remote desktop for '--connect', '--play', or direct connection
|
||||
connect(Get.context!, id, forceRelay: forceRelay, password: password);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> args = List.empty(growable: true);
|
||||
@@ -2342,6 +2360,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
connectMainDesktop(String id,
|
||||
{required bool isFileTransfer,
|
||||
required bool isViewCamera,
|
||||
required bool isTerminal,
|
||||
required bool isTcpTunneling,
|
||||
required bool isRDP,
|
||||
bool? forceRelay,
|
||||
@@ -2366,6 +2385,12 @@ connectMainDesktop(String id,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTerminal) {
|
||||
await rustDeskWinManager.newTerminal(id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else {
|
||||
await rustDeskWinManager.newRemoteDesktop(id,
|
||||
password: password,
|
||||
@@ -2382,6 +2407,7 @@ connectMainDesktop(String id,
|
||||
connect(BuildContext context, String id,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTerminal = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool forceRelay = false,
|
||||
@@ -2404,7 +2430,7 @@ connect(BuildContext context, String id,
|
||||
id = id.replaceAll(' ', '');
|
||||
final oldId = id;
|
||||
id = await bind.mainHandleRelayId(id: id);
|
||||
final forceRelay2 = id != oldId || forceRelay;
|
||||
forceRelay = id != oldId || forceRelay;
|
||||
assert(!(isFileTransfer && isTcpTunneling && isRDP),
|
||||
"more than one connect type");
|
||||
|
||||
@@ -2414,17 +2440,19 @@ connect(BuildContext context, String id,
|
||||
id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay2,
|
||||
forceRelay: forceRelay,
|
||||
);
|
||||
} else {
|
||||
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
|
||||
'id': id,
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera,
|
||||
'isTerminal': isTerminal,
|
||||
'isTcpTunneling': isTcpTunneling,
|
||||
'isRDP': isRDP,
|
||||
'password': password,
|
||||
@@ -2458,7 +2486,10 @@ connect(BuildContext context, String id,
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2473,7 +2504,6 @@ connect(BuildContext context, String id,
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
@@ -2483,10 +2513,25 @@ connect(BuildContext context, String id,
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ViewCameraPage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (isTerminal) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => TerminalPage(
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (isWeb) {
|
||||
Navigator.push(
|
||||
@@ -2497,7 +2542,6 @@ connect(BuildContext context, String id,
|
||||
id: id,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
),
|
||||
),
|
||||
@@ -2507,7 +2551,10 @@ connect(BuildContext context, String id,
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => RemotePage(
|
||||
id: id, password: password, isSharedPassword: isSharedPassword),
|
||||
id: id,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,6 +491,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false,
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
@@ -506,6 +507,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
isTerminal: isTerminal,
|
||||
);
|
||||
},
|
||||
padding: menuPadding,
|
||||
@@ -541,6 +543,15 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _terminalAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
translate('Terminal'),
|
||||
isTerminal: true,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _tcpTunnelingAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
@@ -892,6 +903,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -952,6 +964,7 @@ class FavoritePeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1006,6 +1019,7 @@ class DiscoveredPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
|
||||
final List favs = (await bind.mainGetFav()).toList();
|
||||
@@ -1060,6 +1074,7 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1195,6 +1210,7 @@ class MyGroupPeerCard extends BasePeerCard {
|
||||
_connectAction(context),
|
||||
_transferFileAction(context),
|
||||
_viewCameraAction(context),
|
||||
_terminalAction(context),
|
||||
];
|
||||
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
|
||||
menuItems.add(_tcpTunnelingAction(context));
|
||||
@@ -1420,7 +1436,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false}) async {
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false}) async {
|
||||
var password = '';
|
||||
bool isSharedPassword = false;
|
||||
if (tab == PeerTabIndex.ab) {
|
||||
@@ -1444,6 +1461,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isTerminal: isTerminal,
|
||||
isViewCamera: isViewCamera,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP);
|
||||
|
||||
@@ -243,7 +243,8 @@ List<(String, String)> otherDefaultSettings() {
|
||||
(
|
||||
'Use all my displays for the remote session',
|
||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||
)
|
||||
),
|
||||
('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
|
||||
];
|
||||
|
||||
return v;
|
||||
|
||||
@@ -154,36 +154,38 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
onPressed: () => ffi.cursorModel.reset()));
|
||||
}
|
||||
|
||||
// https://github.com/rustdesk/rustdesk/pull/9731
|
||||
// Does not work for connection established by "accept".
|
||||
connectWithToken(
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false}) {
|
||||
bool isTcpTunneling = false,
|
||||
bool isTerminal = false}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
connToken: connToken);
|
||||
}
|
||||
|
||||
// transferFile
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Transfer file')),
|
||||
onPressed: () => connectWithToken(isFileTransfer: true)),
|
||||
);
|
||||
}
|
||||
// viewCamera
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('View camera')),
|
||||
onPressed: () => connectWithToken(isViewCamera: true)),
|
||||
);
|
||||
}
|
||||
// tcpTunneling
|
||||
if (isDefaultConn && isDesktop) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Terminal')),
|
||||
onPressed: () => connectWithToken(isTerminal: true)),
|
||||
);
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('TCP tunneling')),
|
||||
|
||||
@@ -27,7 +27,6 @@ const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||
"supported_privacy_mode_impl";
|
||||
const String kPlatformAdditionsSupportViewCamera = "support_view_camera";
|
||||
|
||||
const String kPeerPlatformWindows = "Windows";
|
||||
const String kPeerPlatformLinux = "Linux";
|
||||
@@ -47,6 +46,7 @@ const String kAppTypeDesktopRemote = "remote";
|
||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||
const String kAppTypeDesktopViewCamera = "view camera";
|
||||
const String kAppTypeDesktopPortForward = "port forward";
|
||||
const String kAppTypeDesktopTerminal = "terminal";
|
||||
|
||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||
const String kWindowGetWindowInfo = "get_window_info";
|
||||
@@ -62,6 +62,7 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
|
||||
const String kWindowEventNewFileTransfer = "new_file_transfer";
|
||||
const String kWindowEventNewViewCamera = "new_view_camera";
|
||||
const String kWindowEventNewPortForward = "new_port_forward";
|
||||
const String kWindowEventNewTerminal = "new_terminal";
|
||||
const String kWindowEventActiveSession = "active_session";
|
||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||
@@ -103,6 +104,8 @@ const String kOptionEnableClipboard = "enable-clipboard";
|
||||
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||
const String kOptionEnableAudio = "enable-audio";
|
||||
const String kOptionEnableCamera = "enable-camera";
|
||||
const String kOptionEnableTerminal = "enable-terminal";
|
||||
const String kOptionTerminalPersistent = "terminal-persistent";
|
||||
const String kOptionEnableTunnel = "enable-tunnel";
|
||||
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||
const String kOptionEnableBlockInput = "enable-block-input";
|
||||
|
||||
@@ -327,10 +327,15 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
|
||||
/// Callback for the connect button.
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) {
|
||||
void onConnect(
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTerminal = false}) {
|
||||
var id = _idController.id;
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer, isViewCamera: isViewCamera);
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal);
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
@@ -527,22 +532,23 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Obx(() {
|
||||
var offset = Offset(0, 0);
|
||||
return InkWell(
|
||||
child: _menuOpen.value
|
||||
? Transform.rotate(
|
||||
angle: pi,
|
||||
child: Icon(IconFont.more, size: 14),
|
||||
)
|
||||
: Icon(IconFont.more, size: 14),
|
||||
onTapDown: (e) {
|
||||
offset = e.globalPosition;
|
||||
},
|
||||
onTap: () async {
|
||||
_menuOpen.value = true;
|
||||
final x = offset.dx;
|
||||
final y = offset.dy;
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
var offset = Offset(0, 0);
|
||||
return Obx(() => InkWell(
|
||||
child: _menuOpen.value
|
||||
? Transform.rotate(
|
||||
angle: pi,
|
||||
child: Icon(IconFont.more, size: 14),
|
||||
)
|
||||
: Icon(IconFont.more, size: 14),
|
||||
onTapDown: (e) {
|
||||
offset = e.globalPosition;
|
||||
},
|
||||
onTap: () async {
|
||||
_menuOpen.value = true;
|
||||
final x = offset.dx;
|
||||
final y = offset.dy;
|
||||
await mod_menu
|
||||
.showMenu(
|
||||
context: context,
|
||||
@@ -556,6 +562,10 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
'View camera',
|
||||
() => onConnect(isViewCamera: true)
|
||||
),
|
||||
(
|
||||
'Terminal',
|
||||
() => onConnect(isTerminal: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
@@ -583,8 +593,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
_menuOpen.value = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
@@ -786,6 +786,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
call.arguments['id'],
|
||||
isFileTransfer: call.arguments['isFileTransfer'],
|
||||
isViewCamera: call.arguments['isViewCamera'],
|
||||
isTerminal: call.arguments['isTerminal'],
|
||||
isTcpTunneling: call.arguments['isTcpTunneling'],
|
||||
isRDP: call.arguments['isRDP'],
|
||||
password: call.arguments['password'],
|
||||
|
||||
@@ -1011,6 +1011,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable camera', kOptionEnableCamera,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
_OptionCheckBox(
|
||||
context, 'Enable TCP tunneling', kOptionEnableTunnel,
|
||||
enabled: enabled, fakeValue: fakeValue),
|
||||
|
||||
@@ -355,6 +355,7 @@ Widget buildConnectionCard(Client client) {
|
||||
_CmHeader(client: client),
|
||||
client.type_() == ClientType.file ||
|
||||
client.type_() == ClientType.portForward ||
|
||||
client.type_() == ClientType.terminal ||
|
||||
client.disconnected
|
||||
? Offstage()
|
||||
: _PrivilegeBoard(client: client),
|
||||
@@ -499,7 +500,36 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
"(${client.peerId})",
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
).marginOnly(bottom: 10.0),
|
||||
),
|
||||
if (client.type_() == ClientType.terminal)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("Terminal"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.type_() == ClientType.file)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("File Transfer"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.type_() == ClientType.camera)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
translate("View Camera"),
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (client.portForward.isNotEmpty)
|
||||
FittedBox(
|
||||
child: Text(
|
||||
"Port Forward: ${client.portForward}",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.0),
|
||||
FittedBox(
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../models/model.dart';
|
||||
|
||||
/// Manages terminal connections to ensure one FFI instance per peer
|
||||
class TerminalConnectionManager {
|
||||
static final Map<String, FFI> _connections = {};
|
||||
static final Map<String, int> _connectionRefCount = {};
|
||||
|
||||
// Track service IDs per peer
|
||||
static final Map<String, String> _serviceIds = {};
|
||||
|
||||
/// Get or create an FFI instance for a peer
|
||||
static FFI getConnection({
|
||||
required String peerId,
|
||||
required String? password,
|
||||
required bool? isSharedPassword,
|
||||
required bool? forceRelay,
|
||||
required String? connToken,
|
||||
}) {
|
||||
final existingFfi = _connections[peerId];
|
||||
if (existingFfi != null && !existingFfi.closed) {
|
||||
// Increment reference count
|
||||
_connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1;
|
||||
debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}');
|
||||
return existingFfi;
|
||||
}
|
||||
|
||||
// Create new FFI instance for first terminal
|
||||
debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId');
|
||||
final ffi = FFI(null);
|
||||
ffi.start(
|
||||
peerId,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay,
|
||||
connToken: connToken,
|
||||
isTerminal: true,
|
||||
);
|
||||
|
||||
_connections[peerId] = ffi;
|
||||
_connectionRefCount[peerId] = 1;
|
||||
|
||||
// Register the FFI instance with Get for dependency injection
|
||||
Get.put<FFI>(ffi, tag: 'terminal_$peerId');
|
||||
|
||||
debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}');
|
||||
return ffi;
|
||||
}
|
||||
|
||||
/// Release a connection reference
|
||||
static void releaseConnection(String peerId) {
|
||||
final refCount = _connectionRefCount[peerId] ?? 0;
|
||||
debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount');
|
||||
|
||||
if (refCount <= 1) {
|
||||
// Last reference, close the connection
|
||||
final ffi = _connections[peerId];
|
||||
if (ffi != null) {
|
||||
debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)');
|
||||
ffi.close();
|
||||
_connections.remove(peerId);
|
||||
_connectionRefCount.remove(peerId);
|
||||
Get.delete<FFI>(tag: 'terminal_$peerId');
|
||||
}
|
||||
} else {
|
||||
// Decrement reference count
|
||||
_connectionRefCount[peerId] = refCount - 1;
|
||||
debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a connection exists for a peer
|
||||
static bool hasConnection(String peerId) {
|
||||
final ffi = _connections[peerId];
|
||||
return ffi != null && !ffi.closed;
|
||||
}
|
||||
|
||||
/// Get existing connection without creating new one
|
||||
static FFI? getExistingConnection(String peerId) {
|
||||
return _connections[peerId];
|
||||
}
|
||||
|
||||
/// Get connection count for debugging
|
||||
static int getConnectionCount() => _connections.length;
|
||||
|
||||
/// Get terminal count for a peer
|
||||
static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0;
|
||||
|
||||
/// Get service ID for a peer
|
||||
static String? getServiceId(String peerId) => _serviceIds[peerId];
|
||||
|
||||
/// Set service ID for a peer
|
||||
static void setServiceId(String peerId, String serviceId) {
|
||||
_serviceIds[peerId] = serviceId;
|
||||
debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import 'terminal_connection_manager.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.tabController,
|
||||
required this.isSharedPassword,
|
||||
required this.terminalId,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final DesktopTabController tabController;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() => _TerminalPageState();
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
);
|
||||
|
||||
// Create terminal model with specific terminal ID
|
||||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController.onSelected?.call(widget.id);
|
||||
|
||||
// Check if this is a new connection or additional terminal
|
||||
// Note: When a connection exists, the ref count will be > 1 after this terminal is added
|
||||
final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) &&
|
||||
TerminalConnectionManager.getTerminalCount(widget.id) > 1;
|
||||
|
||||
if (!isExistingConnection) {
|
||||
// First terminal - show loading dialog, wait for onReady
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
} else {
|
||||
// Additional terminal - connection already established
|
||||
// Open the terminal directly
|
||||
_terminalModel.openTerminal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
// Release connection reference instead of closing directly
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../models/platform_model.dart';
|
||||
import 'terminal_page.dart';
|
||||
import 'terminal_connection_manager.dart';
|
||||
import '../widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
import '../widgets/popup_menu.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
|
||||
class TerminalTabPage extends StatefulWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
const TerminalTabPage({Key? key, required this.params}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TerminalTabPage> createState() => _TerminalTabPageState(params);
|
||||
}
|
||||
|
||||
class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
||||
|
||||
static const IconData selectedIcon = Icons.terminal;
|
||||
static const IconData unselectedIcon = Icons.terminal_outlined;
|
||||
int _nextTerminalId = 1;
|
||||
|
||||
_TerminalTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
|
||||
tabController.onSelected = (id) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setTitle(getWindowNameWithId(id));
|
||||
};
|
||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: params['id'],
|
||||
terminalId: terminalId,
|
||||
password: params['password'],
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
));
|
||||
}
|
||||
|
||||
TabInfo _createTerminalTab({
|
||||
required String peerId,
|
||||
required int terminalId,
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) {
|
||||
final tabKey = '${peerId}_$terminalId';
|
||||
return TabInfo(
|
||||
key: tabKey,
|
||||
label: '$peerId #$terminalId',
|
||||
selectedIcon: selectedIcon,
|
||||
unselectedIcon: unselectedIcon,
|
||||
onTabCloseButton: () async {
|
||||
// Close the terminal session first
|
||||
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
|
||||
if (ffi != null) {
|
||||
final terminalModel = ffi.terminalModels[terminalId];
|
||||
if (terminalModel != null) {
|
||||
await terminalModel.closeTerminal();
|
||||
}
|
||||
}
|
||||
// Then close the tab
|
||||
tabController.closeBy(tabKey);
|
||||
},
|
||||
page: TerminalPage(
|
||||
key: ValueKey(tabKey),
|
||||
id: peerId,
|
||||
terminalId: terminalId,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
tabController: tabController,
|
||||
forceRelay: forceRelay,
|
||||
connToken: connToken,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
|
||||
final List<MenuEntryBase<String>> menu = [];
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
|
||||
|
||||
// New tab menu item
|
||||
menu.add(MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('New tab'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
_addNewTerminal(peerId);
|
||||
cancelFunc();
|
||||
// Also try to close any BotToast overlays
|
||||
BotToast.cleanAll();
|
||||
},
|
||||
padding: padding,
|
||||
));
|
||||
|
||||
menu.add(MenuEntryDivider());
|
||||
|
||||
menu.add(MenuEntrySwitch<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: translate('Keep terminal sessions on disconnect'),
|
||||
getter: () async {
|
||||
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
|
||||
return bind.sessionGetToggleOptionSync(
|
||||
sessionId: ffi.sessionId,
|
||||
arg: kOptionTerminalPersistent,
|
||||
);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
final ffi = Get.find<FFI>(tag: 'terminal_$peerId');
|
||||
bind.sessionToggleOption(
|
||||
sessionId: ffi.sessionId,
|
||||
value: kOptionTerminalPersistent,
|
||||
);
|
||||
},
|
||||
padding: padding,
|
||||
));
|
||||
|
||||
return mod_menu.PopupMenu<String>(
|
||||
items: menu
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: CustomPopupMenuTheme.commonColor,
|
||||
height: CustomPopupMenuTheme.height,
|
||||
dividerHeight: CustomPopupMenuTheme.dividerHeight,
|
||||
),
|
||||
))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Add keyboard shortcut handler
|
||||
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
|
||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||
print(
|
||||
"[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||
if (call.method == kWindowEventNewTerminal) {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
windowOnTop(windowId());
|
||||
// Allow multiple terminals for the same connection
|
||||
final terminalId = args['terminalId'] ?? _nextTerminalId++;
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: id,
|
||||
terminalId: terminalId,
|
||||
password: args['password'],
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
));
|
||||
} else if (call.method == "onDestroy") {
|
||||
tabController.clear();
|
||||
} else if (call.method == kWindowActionRebuild) {
|
||||
reloadCurrentWindow();
|
||||
}
|
||||
});
|
||||
Future.delayed(Duration.zero, () {
|
||||
restoreWindowPosition(WindowType.Terminal, windowId: windowId());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
if (event is KeyDownEvent) {
|
||||
// Use Cmd+T on macOS, Ctrl+Shift+T on other platforms
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyT) {
|
||||
if (isMacOS &&
|
||||
HardwareKeyboard.instance.isMetaPressed &&
|
||||
!HardwareKeyboard.instance.isShiftPressed) {
|
||||
// macOS: Cmd+T (standard for new tab)
|
||||
_addNewTerminalForCurrentPeer();
|
||||
return true;
|
||||
} else if (!isMacOS &&
|
||||
HardwareKeyboard.instance.isControlPressed &&
|
||||
HardwareKeyboard.instance.isShiftPressed) {
|
||||
// Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal)
|
||||
_addNewTerminalForCurrentPeer();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use Cmd+W on macOS, Ctrl+Shift+W on other platforms
|
||||
if (event.logicalKey == LogicalKeyboardKey.keyW) {
|
||||
if (isMacOS &&
|
||||
HardwareKeyboard.instance.isMetaPressed &&
|
||||
!HardwareKeyboard.instance.isShiftPressed) {
|
||||
// macOS: Cmd+W (standard for close tab)
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
if (tabController.state.value.tabs.length > 1) {
|
||||
tabController.closeBy(currentTab.key);
|
||||
return true;
|
||||
}
|
||||
} else if (!isMacOS &&
|
||||
HardwareKeyboard.instance.isControlPressed &&
|
||||
HardwareKeyboard.instance.isShiftPressed) {
|
||||
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
if (tabController.state.value.tabs.length > 1) {
|
||||
tabController.closeBy(currentTab.key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use Alt+Left/Right for tab navigation (avoids conflicts)
|
||||
if (HardwareKeyboard.instance.isAltPressed) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||
// Previous tab
|
||||
final currentIndex = tabController.state.value.selected;
|
||||
if (currentIndex > 0) {
|
||||
tabController.jumpTo(currentIndex - 1);
|
||||
}
|
||||
return true;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
// Next tab
|
||||
final currentIndex = tabController.state.value.selected;
|
||||
if (currentIndex < tabController.length - 1) {
|
||||
tabController.jumpTo(currentIndex + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Cmd/Ctrl + Number (switch to specific tab)
|
||||
final numberKeys = [
|
||||
LogicalKeyboardKey.digit1,
|
||||
LogicalKeyboardKey.digit2,
|
||||
LogicalKeyboardKey.digit3,
|
||||
LogicalKeyboardKey.digit4,
|
||||
LogicalKeyboardKey.digit5,
|
||||
LogicalKeyboardKey.digit6,
|
||||
LogicalKeyboardKey.digit7,
|
||||
LogicalKeyboardKey.digit8,
|
||||
LogicalKeyboardKey.digit9,
|
||||
];
|
||||
|
||||
for (int i = 0; i < numberKeys.length; i++) {
|
||||
if (event.logicalKey == numberKeys[i] &&
|
||||
((isMacOS && HardwareKeyboard.instance.isMetaPressed) ||
|
||||
(!isMacOS && HardwareKeyboard.instance.isControlPressed))) {
|
||||
if (i < tabController.length) {
|
||||
tabController.jumpTo(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _addNewTerminal(String peerId) {
|
||||
// Find first tab for this peer to get connection parameters
|
||||
final firstTab = tabController.state.value.tabs.firstWhere(
|
||||
(tab) => tab.key.startsWith('$peerId\_'),
|
||||
);
|
||||
if (firstTab.page is TerminalPage) {
|
||||
final page = firstTab.page as TerminalPage;
|
||||
final terminalId = _nextTerminalId++;
|
||||
tabController.add(_createTerminalTab(
|
||||
peerId: peerId,
|
||||
terminalId: terminalId,
|
||||
password: page.password,
|
||||
isSharedPassword: page.isSharedPassword,
|
||||
forceRelay: page.forceRelay,
|
||||
connToken: page.connToken,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _addNewTerminalForCurrentPeer() {
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
final parts = currentTab.key.split('_');
|
||||
if (parts.isNotEmpty) {
|
||||
final peerId = parts[0];
|
||||
_addNewTerminal(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = Scaffold(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: _buildAddButton(),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabMenuBuilder: (key) {
|
||||
// Extract peerId from tab key (format: "peerId_terminalId")
|
||||
final parts = key.split('_');
|
||||
if (parts.isEmpty) return Container();
|
||||
final peerId = parts[0];
|
||||
return _tabMenuBuilder(peerId, () {});
|
||||
},
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: workaroundWindowBorder(
|
||||
context,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
enableResizeEdges: subWindowManagerEnableResizeEdges,
|
||||
windowId: stateGlobal.windowId,
|
||||
);
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
WindowController.fromWindowId(windowId()).close();
|
||||
}
|
||||
}
|
||||
|
||||
int windowId() {
|
||||
return widget.params["windowId"];
|
||||
}
|
||||
|
||||
Widget _buildAddButton() {
|
||||
return ActionIcon(
|
||||
message: 'New tab',
|
||||
icon: IconFont.add,
|
||||
onTap: () {
|
||||
_addNewTerminalForCurrentPeer();
|
||||
},
|
||||
isClose: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> handleWindowCloseButton() async {
|
||||
final connLength = tabController.state.value.tabs.length;
|
||||
if (connLength <= 1) {
|
||||
tabController.clear();
|
||||
return true;
|
||||
} else {
|
||||
final bool res;
|
||||
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||
res = true;
|
||||
} else {
|
||||
res = await closeConfirmDialog();
|
||||
}
|
||||
if (res) {
|
||||
tabController.clear();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,8 +515,6 @@ class ImagePaint extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ImagePaintState extends State<ImagePaint> {
|
||||
bool _lastRemoteCursorMoved = false;
|
||||
|
||||
String get id => widget.id;
|
||||
RxBool get cursorOverImage => widget.cursorOverImage;
|
||||
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart';
|
||||
|
||||
class DesktopTerminalScreen extends StatelessWidget {
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
const DesktopTerminalScreen({Key? key, required this.params})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: isLinux ? Colors.transparent : null,
|
||||
body: TerminalTabPage(
|
||||
params: params,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ enum DesktopTabType {
|
||||
fileTransfer,
|
||||
viewCamera,
|
||||
portForward,
|
||||
terminal,
|
||||
install,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/screen/desktop_terminal_screen.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
@@ -91,6 +92,12 @@ Future<void> main(List<String> args) async {
|
||||
kAppTypeDesktopPortForward,
|
||||
);
|
||||
break;
|
||||
case WindowType.Terminal:
|
||||
desktopType = DesktopType.terminal;
|
||||
runMultiWindow(
|
||||
argument,
|
||||
kAppTypeDesktopTerminal,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -211,6 +218,11 @@ void runMultiWindow(
|
||||
params: argument,
|
||||
);
|
||||
break;
|
||||
case kAppTypeDesktopTerminal:
|
||||
widget = DesktopTerminalScreen(
|
||||
params: argument,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// no such appType
|
||||
exit(0);
|
||||
@@ -257,6 +269,9 @@ void runMultiWindow(
|
||||
case kAppTypeDesktopPortForward:
|
||||
await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
|
||||
break;
|
||||
case kAppTypeDesktopTerminal:
|
||||
await restoreWindowPosition(WindowType.Terminal, windowId: kWindowId!);
|
||||
break;
|
||||
default:
|
||||
// no such appType
|
||||
exit(0);
|
||||
|
||||
@@ -12,11 +12,12 @@ import '../../common/widgets/dialog.dart';
|
||||
|
||||
class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FileManagerPageState();
|
||||
@@ -74,7 +75,8 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
gFFI.start(widget.id,
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword);
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
|
||||
@@ -205,13 +205,13 @@ class WebHomePage extends StatelessWidget {
|
||||
}
|
||||
bool isFileTransfer = false;
|
||||
bool isViewCamera = false;
|
||||
bool isTerminal = false;
|
||||
String? id;
|
||||
String? password;
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--connect':
|
||||
case '--play':
|
||||
isFileTransfer = false;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
@@ -225,6 +225,11 @@ class WebHomePage extends StatelessWidget {
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--terminal':
|
||||
isTerminal = true;
|
||||
id = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--password':
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
@@ -234,7 +239,11 @@ class WebHomePage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password);
|
||||
connect(context, id,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
password: password);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,12 +40,13 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||
}
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
RemotePage({Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
|
||||
@override
|
||||
State<RemotePage> createState() => _RemotePageState(id);
|
||||
@@ -89,6 +90,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'home_page.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget implements PageShape {
|
||||
@override
|
||||
final title = translate("Share Screen");
|
||||
final title = translate("Share screen");
|
||||
|
||||
@override
|
||||
final icon = const Icon(Icons.mobile_screen_share);
|
||||
@@ -649,8 +649,8 @@ class ConnectionManager extends StatelessWidget {
|
||||
children: serverModel.clients
|
||||
.map((client) => PaddingCard(
|
||||
title: translate(client.isFileTransfer
|
||||
? "File Connection"
|
||||
: "Screen Connection"),
|
||||
? "Transfer file"
|
||||
: "Share screen"),
|
||||
titleIcon: client.isFileTransfer
|
||||
? Icon(Icons.folder_outlined)
|
||||
: Icon(Icons.mobile_screen_share),
|
||||
|
||||
@@ -815,7 +815,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
!outgoingOnly &&
|
||||
!hideSecuritySettings)
|
||||
SettingsSection(
|
||||
title: Text(translate("Share Screen")),
|
||||
title: Text(translate("Share screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import '../../desktop/pages/terminal_connection_manager.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final terminalId = 0;
|
||||
|
||||
@override
|
||||
State<TerminalPage> createState() => _TerminalPageState();
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
debugPrint(
|
||||
'[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}');
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
);
|
||||
|
||||
// Create terminal model with specific terminal ID
|
||||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
super.dispose();
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -39,12 +39,13 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||
|
||||
class ViewCameraPage extends StatefulWidget {
|
||||
ViewCameraPage(
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword})
|
||||
{Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
|
||||
@override
|
||||
State<ViewCameraPage> createState() => _ViewCameraPageState(id);
|
||||
@@ -88,6 +89,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
isViewCamera: true,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/models/user_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:flutter_hbb/plugin/event.dart';
|
||||
import 'package:flutter_hbb/plugin/manager.dart';
|
||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||
@@ -311,6 +312,8 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'chat_server_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(int.parse(evt['id'] as String), evt['text'] ?? '');
|
||||
} else if (name == 'terminal_response') {
|
||||
parent.target?.routeTerminalResponse(evt);
|
||||
} else if (name == 'file_dir') {
|
||||
parent.target?.fileModel.receiveFileDir(evt);
|
||||
} else if (name == 'empty_dirs') {
|
||||
@@ -1076,9 +1079,14 @@ class FfiModel with ChangeNotifier {
|
||||
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||
'';
|
||||
}
|
||||
// FIXME: handle ViewCamera ConnType independently.
|
||||
if (connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
} else if (connType == ConnType.terminal) {
|
||||
// Call onReady on all registered terminal models
|
||||
final models = parent.target?._terminalModels.values ?? [];
|
||||
for (final model in models) {
|
||||
model.onReady();
|
||||
}
|
||||
} else if (connType == ConnType.defaultConn ||
|
||||
connType == ConnType.viewCamera) {
|
||||
List<Display> newDisplays = [];
|
||||
@@ -2828,7 +2836,14 @@ class ElevationModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
// The index values of `ConnType` are same as rust protobuf.
|
||||
enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera }
|
||||
enum ConnType {
|
||||
defaultConn,
|
||||
fileTransfer,
|
||||
portForward,
|
||||
rdp,
|
||||
viewCamera,
|
||||
terminal
|
||||
}
|
||||
|
||||
/// Flutter state manager and data communication with the Rust core.
|
||||
class FFI {
|
||||
@@ -2863,6 +2878,12 @@ class FFI {
|
||||
late final Peers favoritePeersModel; // global
|
||||
late final Peers lanPeersModel; // global
|
||||
|
||||
// Terminal model registry for multiple terminals
|
||||
final Map<int, TerminalModel> _terminalModels = {};
|
||||
|
||||
// Getter for terminal models
|
||||
Map<int, TerminalModel> get terminalModels => _terminalModels;
|
||||
|
||||
FFI(SessionID? sId) {
|
||||
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
||||
imageModel = ImageModel(WeakReference(this));
|
||||
@@ -2910,6 +2931,7 @@ class FFI {
|
||||
bool isViewCamera = false,
|
||||
bool isPortForward = false,
|
||||
bool isRdp = false,
|
||||
bool isTerminal = false,
|
||||
String? switchUuid,
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
@@ -2925,7 +2947,10 @@ class FFI {
|
||||
assert(
|
||||
(!(isPortForward && isViewCamera)) &&
|
||||
(!(isViewCamera && isPortForward)) &&
|
||||
(!(isPortForward && isFileTransfer)),
|
||||
(!(isPortForward && isFileTransfer)) &&
|
||||
(!(isTerminal && isFileTransfer)) &&
|
||||
(!(isTerminal && isViewCamera)) &&
|
||||
(!(isTerminal && isPortForward)),
|
||||
'more than one connect type');
|
||||
if (isFileTransfer) {
|
||||
connType = ConnType.fileTransfer;
|
||||
@@ -2933,6 +2958,8 @@ class FFI {
|
||||
connType = ConnType.viewCamera;
|
||||
} else if (isPortForward) {
|
||||
connType = ConnType.portForward;
|
||||
} else if (isTerminal) {
|
||||
connType = ConnType.terminal;
|
||||
} else {
|
||||
chatModel.resetClientMode();
|
||||
connType = ConnType.defaultConn;
|
||||
@@ -2953,6 +2980,7 @@ class FFI {
|
||||
isViewCamera: isViewCamera,
|
||||
isPortForward: isPortForward,
|
||||
isRdp: isRdp,
|
||||
isTerminal: isTerminal,
|
||||
switchUuid: switchUuid ?? '',
|
||||
forceRelay: forceRelay ?? false,
|
||||
password: password ?? '',
|
||||
@@ -3132,6 +3160,11 @@ class FFI {
|
||||
Future<void> close({bool closeSession = true}) async {
|
||||
closed = true;
|
||||
chatModel.close();
|
||||
// Close all terminal models
|
||||
for (final model in _terminalModels.values) {
|
||||
model.dispose();
|
||||
}
|
||||
_terminalModels.clear();
|
||||
if (imageModel.image != null && !isWebDesktop) {
|
||||
await setCanvasConfig(
|
||||
sessionId,
|
||||
@@ -3162,6 +3195,27 @@ class FFI {
|
||||
Future<bool> invokeMethod(String method, [dynamic arguments]) async {
|
||||
return await platformFFI.invokeMethod(method, arguments);
|
||||
}
|
||||
|
||||
// Terminal model management
|
||||
void registerTerminalModel(int terminalId, TerminalModel model) {
|
||||
debugPrint('[FFI] Registering terminal model for terminal $terminalId');
|
||||
_terminalModels[terminalId] = model;
|
||||
}
|
||||
|
||||
void unregisterTerminalModel(int terminalId) {
|
||||
debugPrint('[FFI] Unregistering terminal model for terminal $terminalId');
|
||||
_terminalModels.remove(terminalId);
|
||||
}
|
||||
|
||||
void routeTerminalResponse(Map<String, dynamic> evt) {
|
||||
final int terminalId = evt['terminal_id'] ?? 0;
|
||||
|
||||
// Route to specific terminal model if it exists
|
||||
final model = _terminalModels[terminalId];
|
||||
if (model != null) {
|
||||
model.handleTerminalResponse(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kInvalidResolutionValue = -1;
|
||||
@@ -3266,9 +3320,6 @@ class PeerInfo with ChangeNotifier {
|
||||
bool get isAmyuniIdd =>
|
||||
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||
|
||||
bool get isSupportViewCamera =>
|
||||
platformAdditions[kPlatformAdditionsSupportViewCamera] == true;
|
||||
|
||||
Display? tryGetDisplay({int? display}) {
|
||||
if (displays.isEmpty) {
|
||||
return null;
|
||||
|
||||
@@ -613,7 +613,13 @@ class ServerModel with ChangeNotifier {
|
||||
void showLoginDialog(Client client) {
|
||||
showClientDialog(
|
||||
client,
|
||||
client.isFileTransfer ? "File Connection" : "Screen Connection",
|
||||
client.isFileTransfer
|
||||
? "Transfer file"
|
||||
: client.isViewCamera
|
||||
? "View camera"
|
||||
: client.isTerminal
|
||||
? "Terminal"
|
||||
: "Share screen",
|
||||
'Do you accept?',
|
||||
'android_new_connection_tip',
|
||||
() => sendLoginResponse(client, false),
|
||||
@@ -692,7 +698,7 @@ class ServerModel with ChangeNotifier {
|
||||
void sendLoginResponse(Client client, bool res) async {
|
||||
if (res) {
|
||||
bind.cmLoginRes(connId: client.id, res: res);
|
||||
if (!client.isFileTransfer) {
|
||||
if (!client.isFileTransfer && !client.isTerminal) {
|
||||
parent.target?.invokeMethod("start_capture");
|
||||
}
|
||||
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||
@@ -806,6 +812,7 @@ enum ClientType {
|
||||
file,
|
||||
camera,
|
||||
portForward,
|
||||
terminal,
|
||||
}
|
||||
|
||||
class Client {
|
||||
@@ -813,6 +820,7 @@ class Client {
|
||||
bool authorized = false;
|
||||
bool isFileTransfer = false;
|
||||
bool isViewCamera = false;
|
||||
bool isTerminal = false;
|
||||
String portForward = "";
|
||||
String name = "";
|
||||
String peerId = ""; // peer user's id,show at app
|
||||
@@ -839,6 +847,7 @@ class Client {
|
||||
isFileTransfer = json['is_file_transfer'];
|
||||
// TODO: no entry then default.
|
||||
isViewCamera = json['is_view_camera'];
|
||||
isTerminal = json['is_terminal'] ?? false;
|
||||
portForward = json['port_forward'];
|
||||
name = json['name'];
|
||||
peerId = json['peer_id'];
|
||||
@@ -861,6 +870,7 @@ class Client {
|
||||
data['authorized'] = authorized;
|
||||
data['is_file_transfer'] = isFileTransfer;
|
||||
data['is_view_camera'] = isViewCamera;
|
||||
data['is_terminal'] = isTerminal;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
data['peer_id'] = peerId;
|
||||
@@ -883,6 +893,8 @@ class Client {
|
||||
return ClientType.file;
|
||||
} else if (isViewCamera) {
|
||||
return ClientType.camera;
|
||||
} else if (isTerminal) {
|
||||
return ClientType.terminal;
|
||||
} else if (portForward.isNotEmpty) {
|
||||
return ClientType.portForward;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
|
||||
import 'model.dart';
|
||||
import 'platform_model.dart';
|
||||
|
||||
class TerminalModel with ChangeNotifier {
|
||||
final String id; // peer id
|
||||
final FFI parent;
|
||||
final int terminalId;
|
||||
late final Terminal terminal;
|
||||
late final TerminalController terminalController;
|
||||
|
||||
bool _terminalOpened = false;
|
||||
bool get terminalOpened => _terminalOpened;
|
||||
|
||||
bool _disposed = false;
|
||||
|
||||
final _inputBuffer = <String>[];
|
||||
|
||||
Future<void> _handleInput(String data) async {
|
||||
if (_terminalOpened) {
|
||||
// Send user input to remote terminal
|
||||
try {
|
||||
await bind.sessionSendTerminalInput(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
data: data,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error sending terminal input: $e');
|
||||
}
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Terminal not opened yet, buffering input');
|
||||
_inputBuffer.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id {
|
||||
terminal = Terminal(maxLines: 10000);
|
||||
terminalController = TerminalController();
|
||||
|
||||
// Setup terminal callbacks
|
||||
terminal.onOutput = _handleInput;
|
||||
|
||||
terminal.onResize = (w, h, pw, ph) async {
|
||||
// Validate all dimensions before using them
|
||||
if (w > 0 && h > 0 && pw > 0 && ph > 0) {
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal resized to ${w}x$h (pixel: ${pw}x$ph)');
|
||||
if (_terminalOpened) {
|
||||
// Notify remote terminal of resize
|
||||
try {
|
||||
await bind.sessionResizeTerminal(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
rows: h,
|
||||
cols: w,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error resizing terminal: $e');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[TerminalModel] Invalid terminal dimensions: ${w}x$h (pixel: ${pw}x$ph)');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void onReady() {
|
||||
parent.dialogManager.dismissAll();
|
||||
|
||||
// Fire and forget - don't block onReady
|
||||
openTerminal().catchError((e) {
|
||||
debugPrint('[TerminalModel] Error opening terminal: $e');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> openTerminal() async {
|
||||
if (_terminalOpened) return;
|
||||
// Request the remote side to open a terminal with default shell
|
||||
// The remote side will decide which shell to use based on its OS
|
||||
|
||||
// Get terminal dimensions, ensuring they are valid
|
||||
int rows = 24;
|
||||
int cols = 80;
|
||||
|
||||
if (terminal.viewHeight > 0) {
|
||||
rows = terminal.viewHeight;
|
||||
}
|
||||
if (terminal.viewWidth > 0) {
|
||||
cols = terminal.viewWidth;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[TerminalModel] Opening terminal $terminalId, sessionId: ${parent.sessionId}, size: ${cols}x$rows');
|
||||
try {
|
||||
await bind
|
||||
.sessionOpenTerminal(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
rows: rows,
|
||||
cols: cols,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
throw TimeoutException(
|
||||
'sessionOpenTerminal timed out after 5 seconds');
|
||||
},
|
||||
);
|
||||
debugPrint('[TerminalModel] sessionOpenTerminal called successfully');
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
|
||||
// Optionally show error to user
|
||||
if (e is TimeoutException) {
|
||||
terminal.write('Failed to open terminal: Connection timeout\r\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeTerminal() async {
|
||||
if (_terminalOpened) {
|
||||
try {
|
||||
await bind
|
||||
.sessionCloseTerminal(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () {
|
||||
throw TimeoutException(
|
||||
'sessionCloseTerminal timed out after 3 seconds');
|
||||
},
|
||||
);
|
||||
debugPrint('[TerminalModel] sessionCloseTerminal called successfully');
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error calling sessionCloseTerminal: $e');
|
||||
// Continue with cleanup even if close fails
|
||||
}
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void handleTerminalResponse(Map<String, dynamic> evt) {
|
||||
final String? type = evt['type'];
|
||||
final int evtTerminalId = evt['terminal_id'] ?? 0;
|
||||
|
||||
// Only handle events for this terminal
|
||||
if (evtTerminalId != terminalId) {
|
||||
debugPrint(
|
||||
'[TerminalModel] Ignoring event for terminal $evtTerminalId (not mine)');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'opened':
|
||||
_handleTerminalOpened(evt);
|
||||
break;
|
||||
case 'data':
|
||||
_handleTerminalData(evt);
|
||||
break;
|
||||
case 'closed':
|
||||
_handleTerminalClosed(evt);
|
||||
break;
|
||||
case 'error':
|
||||
_handleTerminalError(evt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTerminalOpened(Map<String, dynamic> evt) {
|
||||
final bool success = evt['success'] ?? false;
|
||||
final String message = evt['message'] ?? '';
|
||||
final String? serviceId = evt['service_id'];
|
||||
|
||||
debugPrint(
|
||||
'[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId');
|
||||
|
||||
if (success) {
|
||||
_terminalOpened = true;
|
||||
|
||||
// Service ID is now saved on the Rust side in handle_terminal_response
|
||||
|
||||
// Process any buffered input
|
||||
_processBufferedInputAsync().then((_) {
|
||||
notifyListeners();
|
||||
}).catchError((e) {
|
||||
debugPrint('[TerminalModel] Error processing buffered input: $e');
|
||||
notifyListeners();
|
||||
});
|
||||
} else {
|
||||
terminal.write('Failed to open terminal: $message\r\n');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processBufferedInputAsync() async {
|
||||
final buffer = List<String>.from(_inputBuffer);
|
||||
_inputBuffer.clear();
|
||||
|
||||
for (final data in buffer) {
|
||||
try {
|
||||
await bind.sessionSendTerminalInput(
|
||||
sessionId: parent.sessionId,
|
||||
terminalId: terminalId,
|
||||
data: data,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Error sending buffered input: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTerminalData(Map<String, dynamic> evt) {
|
||||
final data = evt['data'];
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
String text = '';
|
||||
if (data is String) {
|
||||
// Try to decode as base64 first
|
||||
try {
|
||||
final bytes = base64Decode(data);
|
||||
text = utf8.decode(bytes);
|
||||
} catch (e) {
|
||||
// If base64 decode fails, treat as plain text
|
||||
text = data;
|
||||
}
|
||||
} else if (data is List) {
|
||||
// Handle if data comes as byte array
|
||||
text = utf8.decode(List<int>.from(data));
|
||||
} else {
|
||||
debugPrint('[TerminalModel] Unknown data type: ${data.runtimeType}');
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.write(text);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||
final int exitCode = evt['exit_code'] ?? 0;
|
||||
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||
final String message = evt['message'] ?? 'Unknown error';
|
||||
terminal.write('\r\nTerminal error: $message\r\n');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// Terminal cleanup is handled server-side when service closes
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ enum WindowType {
|
||||
FileTransfer,
|
||||
ViewCamera,
|
||||
PortForward,
|
||||
Terminal,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -33,6 +34,8 @@ extension Index on int {
|
||||
return WindowType.ViewCamera;
|
||||
case 4:
|
||||
return WindowType.PortForward;
|
||||
case 5:
|
||||
return WindowType.Terminal;
|
||||
default:
|
||||
return WindowType.Unknown;
|
||||
}
|
||||
@@ -61,6 +64,7 @@ class RustDeskMultiWindowManager {
|
||||
final List<int> _fileTransferWindows = List.empty(growable: true);
|
||||
final List<int> _viewCameraWindows = List.empty(growable: true);
|
||||
final List<int> _portForwardWindows = List.empty(growable: true);
|
||||
final List<int> _terminalWindows = List.empty(growable: true);
|
||||
|
||||
moveTabToNewWindow(int windowId, String peerId, String sessionId,
|
||||
WindowType windowType) async {
|
||||
@@ -343,6 +347,32 @@ class RustDeskMultiWindowManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> newTerminal(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
}) async {
|
||||
// Terminal windows should always create new windows, not reuse
|
||||
// This avoids the MissingPluginException when trying to invoke
|
||||
// new_terminal on an inactive window
|
||||
var params = {
|
||||
"type": WindowType.Terminal.index,
|
||||
"id": remoteId,
|
||||
"password": password,
|
||||
"forceRelay": forceRelay,
|
||||
"isSharedPassword": isSharedPassword,
|
||||
"connToken": connToken,
|
||||
};
|
||||
final msg = jsonEncode(params);
|
||||
|
||||
// Always create a new window for terminal
|
||||
final windowId = await newSessionWindow(
|
||||
WindowType.Terminal, remoteId, msg, _terminalWindows, false);
|
||||
return MultiWindowCallResult(windowId, null);
|
||||
}
|
||||
|
||||
Future<MultiWindowCallResult> call(
|
||||
WindowType type, String methodName, dynamic args) async {
|
||||
final wnds = _findWindowsByType(type);
|
||||
@@ -373,6 +403,8 @@ class RustDeskMultiWindowManager {
|
||||
return _viewCameraWindows;
|
||||
case WindowType.PortForward:
|
||||
return _portForwardWindows;
|
||||
case WindowType.Terminal:
|
||||
return _terminalWindows;
|
||||
case WindowType.Unknown:
|
||||
break;
|
||||
}
|
||||
@@ -395,6 +427,8 @@ class RustDeskMultiWindowManager {
|
||||
case WindowType.PortForward:
|
||||
_portForwardWindows.clear();
|
||||
break;
|
||||
case WindowType.Terminal:
|
||||
_terminalWindows.clear();
|
||||
case WindowType.Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ class RustdeskImpl {
|
||||
required bool isViewCamera,
|
||||
required bool isPortForward,
|
||||
required bool isRdp,
|
||||
required bool isTerminal,
|
||||
required String switchUuid,
|
||||
required bool forceRelay,
|
||||
required String password,
|
||||
@@ -94,7 +95,8 @@ class RustdeskImpl {
|
||||
'password': password,
|
||||
'is_shared_password': isSharedPassword,
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera
|
||||
'isViewCamera': isViewCamera,
|
||||
'isTerminal': isTerminal
|
||||
})
|
||||
]);
|
||||
}
|
||||
@@ -1911,5 +1913,63 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("sessionTakeScreenshot");
|
||||
}
|
||||
|
||||
Future<void> sessionOpenTerminal(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
required int rows,
|
||||
required int cols,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'open_terminal',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionSendTerminalInput(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
required String data,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'send_terminal_input',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
'data': data,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionResizeTerminal(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
required int rows,
|
||||
required int cols,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'resize_terminal',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
Future<void> sessionCloseTerminal(
|
||||
{required UuidValue sessionId,
|
||||
required int terminalId,
|
||||
dynamic hint}) {
|
||||
return Future(() => js.context.callMethod('setByName', [
|
||||
'close_terminal',
|
||||
jsonEncode({
|
||||
'terminal_id': terminalId,
|
||||
})
|
||||
]));
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
+31
-21
@@ -10,6 +10,11 @@ PODS:
|
||||
- flutter_custom_cursor (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- FMDB (2.7.12):
|
||||
- FMDB/standard (= 2.7.12)
|
||||
- FMDB/Core (2.7.12)
|
||||
- FMDB/standard (2.7.12):
|
||||
- FMDB/Core
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -17,9 +22,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- screen_retriever (0.0.1):
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
- FlutterMacOS
|
||||
- FMDB (>= 2.7.5)
|
||||
- texture_rgba_renderer (0.0.1):
|
||||
- FlutterMacOS
|
||||
- uni_links_desktop (0.0.1):
|
||||
@@ -46,7 +51,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
|
||||
- texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`)
|
||||
- uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
@@ -55,6 +60,10 @@ DEPENDENCIES:
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
desktop_drop:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
@@ -75,7 +84,7 @@ EXTERNAL SOURCES:
|
||||
screen_retriever:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
|
||||
texture_rgba_renderer:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos
|
||||
uni_links_desktop:
|
||||
@@ -92,24 +101,25 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||
desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43
|
||||
desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a
|
||||
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
flutter_custom_cursor: 37e588711a2746f5cf48adb58b582cacff11c0c6
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6
|
||||
package_info_plus: 122abb51244f66eead59ce7c9c200d6b53111779
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936
|
||||
sqflite: c73556b2499b92f0b6e6946abe4a4084510cdf90
|
||||
texture_rgba_renderer: 6661f577ea5d4990e964c7e3840e544ac798e6da
|
||||
uni_links_desktop: 34322c2646e4c9abc69b62e1865f9782d2850ba2
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
|
||||
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
|
||||
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
+248
-211
File diff suppressed because it is too large.
Load diff
@@ -106,6 +106,8 @@ dependencies:
|
||||
device_info_plus: ^9.1.0
|
||||
qr_flutter: ^4.1.0
|
||||
extended_text: 14.0.0
|
||||
xterm: 4.0.0
|
||||
sqflite: 2.2.0
|
||||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
@@ -118,7 +120,8 @@ dev_dependencies:
|
||||
|
||||
dependency_overrides:
|
||||
intl: ^0.19.0
|
||||
|
||||
flutter_plugin_android_lifecycle: 2.0.17
|
||||
|
||||
# rerun: flutter pub run flutter_launcher_icons
|
||||
flutter_icons:
|
||||
image_path: "../res/icon.png"
|
||||
@@ -193,4 +196,3 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
assets
|
||||
js/src/gen_js_from_hbb.ts
|
||||
js/src/message.ts
|
||||
js/src/rendezvous.ts
|
||||
ogvjs*
|
||||
libopus.js
|
||||
libopus.wasm
|
||||
yuv-canvas*
|
||||
node_modules
|
||||
@@ -1 +0,0 @@
|
||||
v1 is not compatible with current Flutter source code.
|
||||
@@ -1,183 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Remote Desktop.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="RustDesk">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
|
||||
<title>RustDesk</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script src="ogvjs-1.8.6/ogv.js"></script>
|
||||
<script type="module" crossorigin src="js/dist/index.js"></script>
|
||||
<link rel="modulepreload" href="js/dist/vendor.js">
|
||||
<script src="yuv-canvas-1.2.6.js"></script>
|
||||
<style>
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 16px solid #f3f3f3;
|
||||
border-radius: 50%;
|
||||
border: 15px solid;
|
||||
border-top: 16px solid #024eff;
|
||||
border-right: 16px solid white;
|
||||
border-bottom: 16px solid #024eff;
|
||||
border-left: 16px solid white;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loading">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
var serviceWorkerVersion = null;
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = 'main.dart.js';
|
||||
scriptTag.type = 'application/javascript';
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener('load', function () {
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl)
|
||||
.then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener('statechange', () => {
|
||||
if (serviceWorker.state == 'activated') {
|
||||
console.log('Installed new service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!reg.active && (reg.installing || reg.waiting)) {
|
||||
// No active web worker and we have installed or are installing
|
||||
// one for the first time. Simply wait for it to activate.
|
||||
waitForActivation(reg.installing || reg.waiting);
|
||||
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
||||
// When the app updates the serviceWorkerVersion changes, so we
|
||||
// need to ask the service worker to update.
|
||||
console.log('New service worker available.');
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log('Loading app from service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
'Failed to load app from service worker. Falling back to plain <script> tag.',
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="libs/firebase-app.js?8.10.1"></script>
|
||||
<script src="libs/firebase-analytics.js?8.10.1"></script>
|
||||
|
||||
<script>
|
||||
// Your web app's Firebase configuration
|
||||
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyCgehIZk1aFP0E7wZtYRRqrfvNiNAF39-A",
|
||||
authDomain: "rustdesk.firebaseapp.com",
|
||||
databaseURL: "https://rustdesk.firebaseio.com",
|
||||
projectId: "rustdesk",
|
||||
storageBucket: "rustdesk.appspot.com",
|
||||
messagingSenderId: "768133699366",
|
||||
appId: "1:768133699366:web:d50faf0792cb208d7993e7",
|
||||
measurementId: "G-9PEH85N6ZQ"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
firebase.analytics();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
* text=auto
|
||||
@@ -1,9 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*log
|
||||
ogvjs
|
||||
.vscode
|
||||
.yarn
|
||||
@@ -1 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
from tabnanny import check
|
||||
|
||||
def pad_start(s, n, c = ' '):
|
||||
if len(s) >= n:
|
||||
return s
|
||||
return c * (n - len(s)) + s
|
||||
|
||||
def safe_unicode(s):
|
||||
res = ""
|
||||
for c in s:
|
||||
res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0'))
|
||||
return res
|
||||
|
||||
def main():
|
||||
print('export const LANGS = {')
|
||||
for fn in glob.glob('../../../src/lang/*'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
if lang == 'template': continue
|
||||
print(' %s: {'%lang)
|
||||
for ln in open(fn, encoding='utf-8'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('("'):
|
||||
toks = ln.split('", "')
|
||||
assert(len(toks) == 2)
|
||||
a = toks[0][2:]
|
||||
b = toks[1][:-3]
|
||||
print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b)))
|
||||
print(' },')
|
||||
print('}')
|
||||
check_if_retry = ['', False]
|
||||
KEY_MAP = ['', False]
|
||||
for ln in open('../../../src/client.rs', encoding='utf-8'):
|
||||
ln = ln.strip()
|
||||
if 'check_if_retry' in ln:
|
||||
check_if_retry[1] = True
|
||||
continue
|
||||
if ln.startswith('}') and check_if_retry[1]:
|
||||
check_if_retry[1] = False
|
||||
continue
|
||||
if check_if_retry[1]:
|
||||
ln = removeComment(ln)
|
||||
check_if_retry[0] += ln + '\n'
|
||||
if 'KEY_MAP' in ln:
|
||||
KEY_MAP[1] = True
|
||||
continue
|
||||
if '.collect' in ln and KEY_MAP[1]:
|
||||
KEY_MAP[1] = False
|
||||
continue
|
||||
if KEY_MAP[1] and ln.startswith('('):
|
||||
ln = removeComment(ln)
|
||||
toks = ln.split('", Key::')
|
||||
assert(len(toks) == 2)
|
||||
a = toks[0][2:]
|
||||
b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '')
|
||||
KEY_MAP[0] += ' "%s": "%s",\n'%(a, b)
|
||||
print()
|
||||
print('export function checkIfRetry(msgtype: string, title: string, text: string, retry_for_relay: boolean) {')
|
||||
print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0'))
|
||||
print(';}')
|
||||
print()
|
||||
print('export const KEY_MAP: any = {')
|
||||
print(KEY_MAP[0])
|
||||
print('}')
|
||||
for ln in open('../../../Cargo.toml', encoding='utf-8'):
|
||||
if ln.startswith('version ='):
|
||||
print('export const ' + ln)
|
||||
|
||||
|
||||
def removeComment(ln):
|
||||
return re.sub('\s+\/\/.*$', '', ln)
|
||||
|
||||
main()
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg?v2" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="ogvjs-1.8.6/ogv.js"></script>
|
||||
<script src="./yuv-canvas-1.2.6.js"></script>
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "web_hbb",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.4.4",
|
||||
"vite": "^2.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-sha256": "^1.3.0",
|
||||
"libsodium": "^0.7.9",
|
||||
"libsodium-wrappers": "^0.7.9",
|
||||
"pcm-player": "^0.0.11",
|
||||
"ts-proto": "^1.101.0",
|
||||
"wasm-feature-detect": "^1.2.11",
|
||||
"zstddec": "^0.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// example: https://github.com/rgov/js-theora-decoder/blob/main/index.html
|
||||
// https://github.com/brion/ogv.js/releases, yarn add has no simd
|
||||
// dev: copy decoder files from node/ogv/dist/* to project dir
|
||||
// dist: .... to dist
|
||||
/*
|
||||
OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js',
|
||||
OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js',
|
||||
OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js',
|
||||
OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js',
|
||||
OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js',
|
||||
OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js',
|
||||
OGVDecoderVideoVP8MTW: 'ogv-decoder-video-vp8-mt-wasm.js',
|
||||
OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js',
|
||||
OGVDecoderVideoVP9SIMDW: 'ogv-decoder-video-vp9-simd-wasm.js',
|
||||
OGVDecoderVideoVP9MTW: 'ogv-decoder-video-vp9-mt-wasm.js',
|
||||
OGVDecoderVideoVP9SIMDMTW: 'ogv-decoder-video-vp9-simd-mt-wasm.js',
|
||||
OGVDecoderVideoAV1W: 'ogv-decoder-video-av1-wasm.js',
|
||||
OGVDecoderVideoAV1SIMDW: 'ogv-decoder-video-av1-simd-wasm.js',
|
||||
OGVDecoderVideoAV1MTW: 'ogv-decoder-video-av1-mt-wasm.js',
|
||||
OGVDecoderVideoAV1SIMDMTW: 'ogv-decoder-video-av1-simd-mt-wasm.js',
|
||||
*/
|
||||
import { simd } from "wasm-feature-detect";
|
||||
|
||||
export async function loadVp9(callback) {
|
||||
// Multithreading is used only if `options.threading` is true.
|
||||
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
|
||||
// currently available in Firefox and Chrome with experimental flags enabled.
|
||||
// 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer
|
||||
const isSIMD = await simd();
|
||||
console.log('isSIMD: ' + isSIMD);
|
||||
window.OGVLoader.loadClass(
|
||||
isSIMD ? "OGVDecoderVideoVP9SIMDW" : "OGVDecoderVideoVP9W",
|
||||
(videoCodecClass) => {
|
||||
window.videoCodecClass = videoCodecClass;
|
||||
videoCodecClass({ videoFormat: {} }).then((decoder) => {
|
||||
decoder.init(() => {
|
||||
callback(decoder);
|
||||
})
|
||||
})
|
||||
},
|
||||
{ worker: true, threading: true }
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import * as zstd from "zstddec";
|
||||
import { KeyEvent, controlKeyFromJSON, ControlKey } from "./message";
|
||||
import { KEY_MAP, LANGS } from "./gen_js_from_hbb";
|
||||
|
||||
let decompressor: zstd.ZSTDDecoder;
|
||||
|
||||
export async function initZstd() {
|
||||
const tmp = new zstd.ZSTDDecoder();
|
||||
await tmp.init();
|
||||
console.log("zstd ready");
|
||||
decompressor = tmp;
|
||||
}
|
||||
|
||||
export async function decompress(compressedArray: Uint8Array) {
|
||||
const MAX = 1024 * 1024 * 64;
|
||||
const MIN = 1024 * 1024;
|
||||
let n = 30 * compressedArray.length;
|
||||
if (n > MAX) {
|
||||
n = MAX;
|
||||
}
|
||||
if (n < MIN) {
|
||||
n = MIN;
|
||||
}
|
||||
try {
|
||||
if (!decompressor) {
|
||||
await initZstd();
|
||||
}
|
||||
return decompressor.decode(compressedArray, n);
|
||||
} catch (e) {
|
||||
console.error("decompress failed: " + e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const LANG = getLang();
|
||||
|
||||
export function translate(locale: string, text: string): string {
|
||||
const lang = LANG || locale.substring(locale.length - 2).toLowerCase();
|
||||
let en = LANGS.en as any;
|
||||
let dict = (LANGS as any)[lang];
|
||||
if (!dict) dict = en;
|
||||
let res = dict[text];
|
||||
if (!res && lang != "en") res = en[text];
|
||||
return res || text;
|
||||
}
|
||||
|
||||
const zCode = "z".charCodeAt(0);
|
||||
const aCode = "a".charCodeAt(0);
|
||||
|
||||
export function mapKey(name: string, isDesktop: Boolean) {
|
||||
const tmp = KEY_MAP[name] || name;
|
||||
if (tmp.length == 1) {
|
||||
const chr = tmp.charCodeAt(0);
|
||||
if (!isDesktop && (chr > zCode || chr < aCode))
|
||||
return KeyEvent.fromPartial({ unicode: chr });
|
||||
else return KeyEvent.fromPartial({ chr });
|
||||
}
|
||||
const control_key = controlKeyFromJSON(tmp);
|
||||
if (control_key == ControlKey.UNRECOGNIZED) {
|
||||
console.error("Unknown control key " + tmp);
|
||||
}
|
||||
return KeyEvent.fromPartial({ control_key });
|
||||
}
|
||||
|
||||
export async function sleep(ms: number) {
|
||||
await new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function getLang(): string {
|
||||
try {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
return urlParams.get("lang") || "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,773 +0,0 @@
|
||||
import Websock from "./websock";
|
||||
import * as message from "./message.js";
|
||||
import * as rendezvous from "./rendezvous.js";
|
||||
import { loadVp9 } from "./codec";
|
||||
import * as sha256 from "fast-sha256";
|
||||
import * as globals from "./globals";
|
||||
import { decompress, mapKey, sleep } from "./common";
|
||||
|
||||
const PORT = 21116;
|
||||
const HOSTS = [
|
||||
"rs-sg.rustdesk.com",
|
||||
"rs-cn.rustdesk.com",
|
||||
"rs-us.rustdesk.com",
|
||||
];
|
||||
let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
|
||||
const SCHEMA = "ws://";
|
||||
|
||||
type MsgboxCallback = (type: string, title: string, text: string) => void;
|
||||
type DrawCallback = (data: Uint8Array) => void;
|
||||
//const cursorCanvas = document.createElement("canvas");
|
||||
|
||||
export default class Connection {
|
||||
_msgs: any[];
|
||||
_ws: Websock | undefined;
|
||||
_interval: any;
|
||||
_id: string;
|
||||
_hash: message.Hash | undefined;
|
||||
_msgbox: MsgboxCallback;
|
||||
_draw: DrawCallback;
|
||||
_peerInfo: message.PeerInfo | undefined;
|
||||
_firstFrame: Boolean | undefined;
|
||||
_videoDecoder: any;
|
||||
_password: Uint8Array | undefined;
|
||||
_options: any;
|
||||
_videoTestSpeed: number[];
|
||||
//_cursors: { [name: number]: any };
|
||||
|
||||
constructor() {
|
||||
this._msgbox = globals.msgbox;
|
||||
this._draw = globals.draw;
|
||||
this._msgs = [];
|
||||
this._id = "";
|
||||
this._videoTestSpeed = [0, 0];
|
||||
//this._cursors = {};
|
||||
}
|
||||
|
||||
async start(id: string) {
|
||||
try {
|
||||
await this._start(id);
|
||||
} catch (e: any) {
|
||||
this.msgbox(
|
||||
"error",
|
||||
"Connection Error",
|
||||
e.type == "close" ? "Reset by the peer" : String(e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async _start(id: string) {
|
||||
if (!this._options) {
|
||||
this._options = globals.getPeers()[id] || {};
|
||||
}
|
||||
if (!this._password) {
|
||||
const p = this.getOption("password");
|
||||
if (p) {
|
||||
try {
|
||||
this._password = Uint8Array.from(JSON.parse("[" + p + "]"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._interval = setInterval(() => {
|
||||
while (this._msgs.length) {
|
||||
this._ws?.sendMessage(this._msgs[0]);
|
||||
this._msgs.splice(0, 1);
|
||||
}
|
||||
}, 1);
|
||||
this.loadVideoDecoder();
|
||||
const uri = getDefaultUri();
|
||||
const ws = new Websock(uri, true);
|
||||
this._ws = ws;
|
||||
this._id = id;
|
||||
console.log(
|
||||
new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id
|
||||
);
|
||||
await ws.open();
|
||||
console.log(new Date() + ": Connected to rendezvous server");
|
||||
const conn_type = rendezvous.ConnType.DEFAULT_CONN;
|
||||
const nat_type = rendezvous.NatType.SYMMETRIC;
|
||||
const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({
|
||||
id,
|
||||
licence_key: localStorage.getItem("key") || undefined,
|
||||
conn_type,
|
||||
nat_type,
|
||||
token: localStorage.getItem("access_token") || undefined,
|
||||
});
|
||||
ws.sendRendezvous({ punch_hole_request });
|
||||
const msg = (await ws.next()) as rendezvous.RendezvousMessage;
|
||||
ws.close();
|
||||
console.log(new Date() + ": Got relay response");
|
||||
const phr = msg.punch_hole_response;
|
||||
const rr = msg.relay_response;
|
||||
if (phr) {
|
||||
if (phr?.other_failure) {
|
||||
this.msgbox("error", "Error", phr?.other_failure);
|
||||
return;
|
||||
}
|
||||
if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) {
|
||||
switch (phr?.failure) {
|
||||
case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST:
|
||||
this.msgbox("error", "Error", "ID does not exist");
|
||||
break;
|
||||
case rendezvous.PunchHoleResponse_Failure.OFFLINE:
|
||||
this.msgbox("error", "Error", "Remote desktop is offline");
|
||||
break;
|
||||
case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH:
|
||||
this.msgbox("error", "Error", "Key mismatch");
|
||||
break;
|
||||
case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE:
|
||||
this.msgbox("error", "Error", "Key overuse");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (rr) {
|
||||
if (!rr.version) {
|
||||
this.msgbox("error", "Error", "Remote version is low, not support web");
|
||||
return;
|
||||
}
|
||||
await this.connectRelay(rr);
|
||||
}
|
||||
}
|
||||
|
||||
async connectRelay(rr: rendezvous.RelayResponse) {
|
||||
const pk = rr.pk;
|
||||
let uri = rr.relay_server;
|
||||
if (uri) {
|
||||
uri = getrUriFromRs(uri, true, 2);
|
||||
} else {
|
||||
uri = getDefaultUri(true);
|
||||
}
|
||||
const uuid = rr.uuid;
|
||||
console.log(new Date() + ": Connecting to relay server: " + uri);
|
||||
const ws = new Websock(uri, false);
|
||||
await ws.open();
|
||||
console.log(new Date() + ": Connected to relay server");
|
||||
this._ws = ws;
|
||||
const request_relay = rendezvous.RequestRelay.fromPartial({
|
||||
licence_key: localStorage.getItem("key") || undefined,
|
||||
uuid,
|
||||
});
|
||||
ws.sendRendezvous({ request_relay });
|
||||
const secure = (await this.secure(pk)) || false;
|
||||
globals.pushEvent("connection_ready", { secure, direct: false });
|
||||
await this.msgLoop();
|
||||
}
|
||||
|
||||
async secure(pk: Uint8Array | undefined) {
|
||||
if (pk) {
|
||||
const RS_PK = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
|
||||
try {
|
||||
pk = await globals.verify(pk, localStorage.getItem("key") || RS_PK);
|
||||
if (pk) {
|
||||
const idpk = message.IdPk.decode(pk);
|
||||
if (idpk.id == this._id) {
|
||||
pk = idpk.pk;
|
||||
}
|
||||
}
|
||||
if (pk?.length != 32) {
|
||||
pk = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
pk = undefined;
|
||||
}
|
||||
if (!pk)
|
||||
console.error(
|
||||
"Handshake failed: invalid public key from rendezvous server"
|
||||
);
|
||||
}
|
||||
if (!pk) {
|
||||
// send an empty message out in case server is setting up secure and waiting for first message
|
||||
const public_key = message.PublicKey.fromPartial({});
|
||||
this._ws?.sendMessage({ public_key });
|
||||
return;
|
||||
}
|
||||
const msg = (await this._ws?.next()) as message.Message;
|
||||
let signedId: any = msg?.signed_id;
|
||||
if (!signedId) {
|
||||
console.error("Handshake failed: invalid message type");
|
||||
const public_key = message.PublicKey.fromPartial({});
|
||||
this._ws?.sendMessage({ public_key });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
signedId = await globals.verify(signedId.id, Uint8Array.from(pk!));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// fall back to non-secure connection in case pk mismatch
|
||||
console.error("pk mismatch, fall back to non-secure");
|
||||
const public_key = message.PublicKey.fromPartial({});
|
||||
this._ws?.sendMessage({ public_key });
|
||||
return;
|
||||
}
|
||||
const idpk = message.IdPk.decode(signedId);
|
||||
const id = idpk.id;
|
||||
const theirPk = idpk.pk;
|
||||
if (id != this._id!) {
|
||||
console.error("Handshake failed: sign failure");
|
||||
const public_key = message.PublicKey.fromPartial({});
|
||||
this._ws?.sendMessage({ public_key });
|
||||
return;
|
||||
}
|
||||
if (theirPk.length != 32) {
|
||||
console.error(
|
||||
"Handshake failed: invalid public box key length from peer"
|
||||
);
|
||||
const public_key = message.PublicKey.fromPartial({});
|
||||
this._ws?.sendMessage({ public_key });
|
||||
return;
|
||||
}
|
||||
const [mySk, asymmetric_value] = globals.genBoxKeyPair();
|
||||
const secret_key = globals.genSecretKey();
|
||||
const symmetric_value = globals.seal(secret_key, theirPk, mySk);
|
||||
const public_key = message.PublicKey.fromPartial({
|
||||
asymmetric_value,
|
||||
symmetric_value,
|
||||
});
|
||||
this._ws?.sendMessage({ public_key });
|
||||
this._ws?.setSecretKey(secret_key);
|
||||
console.log("secured");
|
||||
return true;
|
||||
}
|
||||
|
||||
async msgLoop() {
|
||||
while (true) {
|
||||
const msg = (await this._ws?.next()) as message.Message;
|
||||
if (msg?.hash) {
|
||||
this._hash = msg?.hash;
|
||||
if (!this._password)
|
||||
this.msgbox("input-password", "Password Required", "");
|
||||
this.login();
|
||||
} else if (msg?.test_delay) {
|
||||
const test_delay = msg?.test_delay;
|
||||
console.log(test_delay);
|
||||
if (!test_delay.from_client) {
|
||||
this._ws?.sendMessage({ test_delay });
|
||||
}
|
||||
} else if (msg?.login_response) {
|
||||
const r = msg?.login_response;
|
||||
if (r.error) {
|
||||
if (r.error == "Wrong Password") {
|
||||
this._password = undefined;
|
||||
this.msgbox(
|
||||
"re-input-password",
|
||||
r.error,
|
||||
"Do you want to enter again?"
|
||||
);
|
||||
} else {
|
||||
this.msgbox("error", "Login Error", r.error);
|
||||
}
|
||||
} else if (r.peer_info) {
|
||||
this.handlePeerInfo(r.peer_info);
|
||||
}
|
||||
} else if (msg?.video_frame) {
|
||||
this.handleVideoFrame(msg?.video_frame!);
|
||||
} else if (msg?.clipboard) {
|
||||
const cb = msg?.clipboard;
|
||||
if (cb.compress) {
|
||||
const c = await decompress(cb.content);
|
||||
if (!c) continue;
|
||||
cb.content = c;
|
||||
}
|
||||
try {
|
||||
globals.copyToClipboard(new TextDecoder().decode(cb.content));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// globals.pushEvent("clipboard", cb);
|
||||
} else if (msg?.cursor_data) {
|
||||
const cd = msg?.cursor_data;
|
||||
const c = await decompress(cd.colors);
|
||||
if (!c) continue;
|
||||
cd.colors = c;
|
||||
globals.pushEvent("cursor_data", cd);
|
||||
/*
|
||||
let ctx = cursorCanvas.getContext("2d");
|
||||
cursorCanvas.width = cd.width;
|
||||
cursorCanvas.height = cd.height;
|
||||
let imgData = new ImageData(
|
||||
new Uint8ClampedArray(c),
|
||||
cd.width,
|
||||
cd.height
|
||||
);
|
||||
ctx?.clearRect(0, 0, cd.width, cd.height);
|
||||
ctx?.putImageData(imgData, 0, 0);
|
||||
let url = cursorCanvas.toDataURL();
|
||||
const img = document.createElement("img");
|
||||
img.src = url;
|
||||
this._cursors[cd.id] = img;
|
||||
//cursorCanvas.width /= 2.;
|
||||
//cursorCanvas.height /= 2.;
|
||||
//ctx?.drawImage(img, cursorCanvas.width, cursorCanvas.height);
|
||||
url = cursorCanvas.toDataURL();
|
||||
document.body.style.cursor =
|
||||
"url(" + url + ")" + cd.hotx + " " + cd.hoty + ", default";
|
||||
console.log(document.body.style.cursor);
|
||||
*/
|
||||
} else if (msg?.cursor_id) {
|
||||
globals.pushEvent("cursor_id", { id: msg?.cursor_id });
|
||||
} else if (msg?.cursor_position) {
|
||||
globals.pushEvent("cursor_position", msg?.cursor_position);
|
||||
} else if (msg?.misc) {
|
||||
if (!this.handleMisc(msg?.misc)) break;
|
||||
} else if (msg?.audio_frame) {
|
||||
globals.playAudio(msg?.audio_frame.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msgbox(type_: string, title: string, text: string) {
|
||||
this._msgbox?.(type_, title, text);
|
||||
}
|
||||
|
||||
draw(frame: any) {
|
||||
this._draw?.(frame);
|
||||
globals.draw(frame);
|
||||
}
|
||||
|
||||
close() {
|
||||
this._msgs = [];
|
||||
clearInterval(this._interval);
|
||||
this._ws?.close();
|
||||
this._videoDecoder?.close();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const misc = message.Misc.fromPartial({ refresh_video: true });
|
||||
this._ws?.sendMessage({ misc });
|
||||
}
|
||||
|
||||
setMsgbox(callback: MsgboxCallback) {
|
||||
this._msgbox = callback;
|
||||
}
|
||||
|
||||
setDraw(callback: DrawCallback) {
|
||||
this._draw = callback;
|
||||
}
|
||||
|
||||
login(password: string | undefined = undefined) {
|
||||
if (password) {
|
||||
const salt = this._hash?.salt;
|
||||
let p = hash([password, salt!]);
|
||||
this._password = p;
|
||||
const challenge = this._hash?.challenge;
|
||||
p = hash([p, challenge!]);
|
||||
this.msgbox("connecting", "Connecting...", "Logging in...");
|
||||
this._sendLoginMessage(p);
|
||||
} else {
|
||||
let p = this._password;
|
||||
if (p) {
|
||||
const challenge = this._hash?.challenge;
|
||||
p = hash([p, challenge!]);
|
||||
}
|
||||
this._sendLoginMessage(p);
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
this.close();
|
||||
await this.start(this._id);
|
||||
}
|
||||
|
||||
_sendLoginMessage(password: Uint8Array | undefined = undefined) {
|
||||
const login_request = message.LoginRequest.fromPartial({
|
||||
username: this._id!,
|
||||
my_id: "web", // to-do
|
||||
my_name: "web", // to-do
|
||||
password,
|
||||
option: this.getOptionMessage(),
|
||||
video_ack_required: true,
|
||||
});
|
||||
this._ws?.sendMessage({ login_request });
|
||||
}
|
||||
|
||||
getOptionMessage(): message.OptionMessage | undefined {
|
||||
let n = 0;
|
||||
const msg = message.OptionMessage.fromPartial({});
|
||||
const q = this.getImageQualityEnum(this.getImageQuality(), true);
|
||||
const yes = message.OptionMessage_BoolOption.Yes;
|
||||
if (q != undefined) {
|
||||
msg.image_quality = q;
|
||||
n += 1;
|
||||
}
|
||||
if (this._options["show-remote-cursor"]) {
|
||||
msg.show_remote_cursor = yes;
|
||||
n += 1;
|
||||
}
|
||||
if (this._options["lock-after-session-end"]) {
|
||||
msg.lock_after_session_end = yes;
|
||||
n += 1;
|
||||
}
|
||||
if (this._options["privacy-mode"]) {
|
||||
msg.privacy_mode = yes;
|
||||
n += 1;
|
||||
}
|
||||
if (this._options["disable-audio"]) {
|
||||
msg.disable_audio = yes;
|
||||
n += 1;
|
||||
}
|
||||
if (this._options["disable-clipboard"]) {
|
||||
msg.disable_clipboard = yes;
|
||||
n += 1;
|
||||
}
|
||||
return n > 0 ? msg : undefined;
|
||||
}
|
||||
|
||||
sendVideoReceived() {
|
||||
const misc = message.Misc.fromPartial({ video_received: true });
|
||||
this._ws?.sendMessage({ misc });
|
||||
}
|
||||
|
||||
handleVideoFrame(vf: message.VideoFrame) {
|
||||
if (!this._firstFrame) {
|
||||
this.msgbox("", "", "");
|
||||
this._firstFrame = true;
|
||||
}
|
||||
if (vf.vp9s) {
|
||||
const dec = this._videoDecoder;
|
||||
var tm = new Date().getTime();
|
||||
var i = 0;
|
||||
const n = vf.vp9s?.frames.length;
|
||||
vf.vp9s.frames.forEach((f) => {
|
||||
dec.processFrame(f.data.slice(0).buffer, (ok: any) => {
|
||||
i++;
|
||||
if (i == n) this.sendVideoReceived();
|
||||
if (ok && dec.frameBuffer && n == i) {
|
||||
this.draw(dec.frameBuffer);
|
||||
const now = new Date().getTime();
|
||||
var elapsed = now - tm;
|
||||
this._videoTestSpeed[1] += elapsed;
|
||||
this._videoTestSpeed[0] += 1;
|
||||
if (this._videoTestSpeed[0] >= 30) {
|
||||
console.log(
|
||||
"video decoder: " +
|
||||
parseInt(
|
||||
"" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
|
||||
)
|
||||
);
|
||||
this._videoTestSpeed = [0, 0];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handlePeerInfo(pi: message.PeerInfo) {
|
||||
this._peerInfo = pi;
|
||||
if (pi.displays.length == 0) {
|
||||
this.msgbox("error", "Remote Error", "No Display");
|
||||
return;
|
||||
}
|
||||
this.msgbox("success", "Successful", "Connected, waiting for image...");
|
||||
globals.pushEvent("peer_info", pi);
|
||||
const p = this.shouldAutoLogin();
|
||||
if (p) this.inputOsPassword(p);
|
||||
const username = this.getOption("info")?.username;
|
||||
if (username && !pi.username) pi.username = username;
|
||||
this.setOption("info", pi);
|
||||
if (this.getRemember()) {
|
||||
if (this._password?.length) {
|
||||
const p = this._password.toString();
|
||||
if (p != this.getOption("password")) {
|
||||
this.setOption("password", p);
|
||||
console.log("remember password of " + this._id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setOption("password", undefined);
|
||||
}
|
||||
}
|
||||
|
||||
shouldAutoLogin(): string {
|
||||
const l = this.getOption("lock-after-session-end");
|
||||
const a = !!this.getOption("auto-login");
|
||||
const p = this.getOption("os-password");
|
||||
if (p && l && a) {
|
||||
return p;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
handleMisc(misc: message.Misc) {
|
||||
if (misc.audio_format) {
|
||||
globals.initAudio(
|
||||
misc.audio_format.channels,
|
||||
misc.audio_format.sample_rate
|
||||
);
|
||||
} else if (misc.chat_message) {
|
||||
globals.pushEvent("chat", { text: misc.chat_message.text });
|
||||
} else if (misc.permission_info) {
|
||||
const p = misc.permission_info;
|
||||
console.info("Change permission " + p.permission + " -> " + p.enabled);
|
||||
let name;
|
||||
switch (p.permission) {
|
||||
case message.PermissionInfo_Permission.Keyboard:
|
||||
name = "keyboard";
|
||||
break;
|
||||
case message.PermissionInfo_Permission.Clipboard:
|
||||
name = "clipboard";
|
||||
break;
|
||||
case message.PermissionInfo_Permission.Audio:
|
||||
name = "audio";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
globals.pushEvent("permission", { [name]: p.enabled });
|
||||
} else if (misc.switch_display) {
|
||||
this.loadVideoDecoder();
|
||||
globals.pushEvent("switch_display", misc.switch_display);
|
||||
} else if (misc.close_reason) {
|
||||
this.msgbox("error", "Connection Error", misc.close_reason);
|
||||
this.close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getRemember(): Boolean {
|
||||
return this._options["remember"] || false;
|
||||
}
|
||||
|
||||
setRemember(v: Boolean) {
|
||||
this.setOption("remember", v);
|
||||
}
|
||||
|
||||
getOption(name: string): any {
|
||||
return this._options[name];
|
||||
}
|
||||
|
||||
setOption(name: string, value: any) {
|
||||
if (value == undefined) {
|
||||
delete this._options[name];
|
||||
} else {
|
||||
this._options[name] = value;
|
||||
}
|
||||
this._options["tm"] = new Date().getTime();
|
||||
const peers = globals.getPeers();
|
||||
peers[this._id] = this._options;
|
||||
localStorage.setItem("peers", JSON.stringify(peers));
|
||||
}
|
||||
|
||||
inputKey(
|
||||
name: string,
|
||||
down: boolean,
|
||||
press: boolean,
|
||||
alt: Boolean,
|
||||
ctrl: Boolean,
|
||||
shift: Boolean,
|
||||
command: Boolean
|
||||
) {
|
||||
const key_event = mapKey(name, globals.isDesktop());
|
||||
if (!key_event) return;
|
||||
if (alt && (name == "VK_MENU" || name == "RAlt")) {
|
||||
alt = false;
|
||||
}
|
||||
if (ctrl && (name == "VK_CONTROL" || name == "RControl")) {
|
||||
ctrl = false;
|
||||
}
|
||||
if (shift && (name == "VK_SHIFT" || name == "RShift")) {
|
||||
shift = false;
|
||||
}
|
||||
if (command && (name == "Meta" || name == "RWin")) {
|
||||
command = false;
|
||||
}
|
||||
key_event.down = down;
|
||||
key_event.press = press;
|
||||
key_event.modifiers = this.getMod(alt, ctrl, shift, command);
|
||||
this._ws?.sendMessage({ key_event });
|
||||
}
|
||||
|
||||
ctrlAltDel() {
|
||||
const key_event = message.KeyEvent.fromPartial({ down: true });
|
||||
if (this._peerInfo?.platform == "Windows") {
|
||||
key_event.control_key = message.ControlKey.CtrlAltDel;
|
||||
} else {
|
||||
key_event.control_key = message.ControlKey.Delete;
|
||||
key_event.modifiers = this.getMod(true, true, false, false);
|
||||
}
|
||||
this._ws?.sendMessage({ key_event });
|
||||
}
|
||||
|
||||
inputString(seq: string) {
|
||||
const key_event = message.KeyEvent.fromPartial({ seq });
|
||||
this._ws?.sendMessage({ key_event });
|
||||
}
|
||||
|
||||
switchDisplay(display: number) {
|
||||
const switch_display = message.SwitchDisplay.fromPartial({ display });
|
||||
const misc = message.Misc.fromPartial({ switch_display });
|
||||
this._ws?.sendMessage({ misc });
|
||||
}
|
||||
|
||||
async inputOsPassword(seq: string) {
|
||||
this.inputMouse();
|
||||
await sleep(50);
|
||||
this.inputMouse(0, 3, 3);
|
||||
await sleep(50);
|
||||
this.inputMouse(1 | (1 << 3));
|
||||
this.inputMouse(2 | (1 << 3));
|
||||
await sleep(1200);
|
||||
const key_event = message.KeyEvent.fromPartial({ press: true, seq });
|
||||
this._ws?.sendMessage({ key_event });
|
||||
}
|
||||
|
||||
lockScreen() {
|
||||
const key_event = message.KeyEvent.fromPartial({
|
||||
down: true,
|
||||
control_key: message.ControlKey.LockScreen,
|
||||
});
|
||||
this._ws?.sendMessage({ key_event });
|
||||
}
|
||||
|
||||
getMod(alt: Boolean, ctrl: Boolean, shift: Boolean, command: Boolean) {
|
||||
const mod: message.ControlKey[] = [];
|
||||
if (alt) mod.push(message.ControlKey.Alt);
|
||||
if (ctrl) mod.push(message.ControlKey.Control);
|
||||
if (shift) mod.push(message.ControlKey.Shift);
|
||||
if (command) mod.push(message.ControlKey.Meta);
|
||||
return mod;
|
||||
}
|
||||
|
||||
inputMouse(
|
||||
mask: number = 0,
|
||||
x: number = 0,
|
||||
y: number = 0,
|
||||
alt: Boolean = false,
|
||||
ctrl: Boolean = false,
|
||||
shift: Boolean = false,
|
||||
command: Boolean = false
|
||||
) {
|
||||
const mouse_event = message.MouseEvent.fromPartial({
|
||||
mask,
|
||||
x,
|
||||
y,
|
||||
modifiers: this.getMod(alt, ctrl, shift, command),
|
||||
});
|
||||
this._ws?.sendMessage({ mouse_event });
|
||||
}
|
||||
|
||||
toggleOption(name: string) {
|
||||
const v = !this._options[name];
|
||||
const option = message.OptionMessage.fromPartial({});
|
||||
const v2 = v
|
||||
? message.OptionMessage_BoolOption.Yes
|
||||
: message.OptionMessage_BoolOption.No;
|
||||
switch (name) {
|
||||
case "show-remote-cursor":
|
||||
option.show_remote_cursor = v2;
|
||||
break;
|
||||
case "disable-audio":
|
||||
option.disable_audio = v2;
|
||||
break;
|
||||
case "disable-clipboard":
|
||||
option.disable_clipboard = v2;
|
||||
break;
|
||||
case "lock-after-session-end":
|
||||
option.lock_after_session_end = v2;
|
||||
break;
|
||||
case "privacy-mode":
|
||||
option.privacy_mode = v2;
|
||||
break;
|
||||
case "block-input":
|
||||
option.block_input = message.OptionMessage_BoolOption.Yes;
|
||||
break;
|
||||
case "unblock-input":
|
||||
option.block_input = message.OptionMessage_BoolOption.No;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (name.indexOf("block-input") < 0) this.setOption(name, v);
|
||||
const misc = message.Misc.fromPartial({ option });
|
||||
this._ws?.sendMessage({ misc });
|
||||
}
|
||||
|
||||
getImageQuality() {
|
||||
return this.getOption("image-quality");
|
||||
}
|
||||
|
||||
getImageQualityEnum(
|
||||
value: string,
|
||||
ignoreDefault: Boolean
|
||||
): message.ImageQuality | undefined {
|
||||
switch (value) {
|
||||
case "low":
|
||||
return message.ImageQuality.Low;
|
||||
case "best":
|
||||
return message.ImageQuality.Best;
|
||||
case "balanced":
|
||||
return ignoreDefault ? undefined : message.ImageQuality.Balanced;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setImageQuality(value: string) {
|
||||
this.setOption("image-quality", value);
|
||||
const image_quality = this.getImageQualityEnum(value, false);
|
||||
if (image_quality == undefined) return;
|
||||
const option = message.OptionMessage.fromPartial({ image_quality });
|
||||
const misc = message.Misc.fromPartial({ option });
|
||||
this._ws?.sendMessage({ misc });
|
||||
}
|
||||
|
||||
loadVideoDecoder() {
|
||||
this._videoDecoder?.close();
|
||||
loadVp9((decoder: any) => {
|
||||
this._videoDecoder = decoder;
|
||||
console.log("vp9 loaded");
|
||||
console.log(decoder);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function testDelay() {
|
||||
var nearest = "";
|
||||
HOSTS.forEach((host) => {
|
||||
const now = new Date().getTime();
|
||||
new Websock(getrUriFromRs(host), true).open().then(() => {
|
||||
console.log("latency of " + host + ": " + (new Date().getTime() - now));
|
||||
if (!nearest) {
|
||||
HOST = host;
|
||||
localStorage.setItem("rendezvous-server", host);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testDelay();
|
||||
|
||||
function getDefaultUri(isRelay: Boolean = false): string {
|
||||
const host = localStorage.getItem("custom-rendezvous-server");
|
||||
return getrUriFromRs(host || HOST, isRelay);
|
||||
}
|
||||
|
||||
function getrUriFromRs(
|
||||
uri: string,
|
||||
isRelay: Boolean = false,
|
||||
roffset: number = 0
|
||||
): string {
|
||||
if (uri.indexOf(":") > 0) {
|
||||
const tmp = uri.split(":");
|
||||
const port = parseInt(tmp[1]);
|
||||
uri = tmp[0] + ":" + (port + (isRelay ? roffset || 3 : 2));
|
||||
} else {
|
||||
uri += ":" + (PORT + (isRelay ? 3 : 2));
|
||||
}
|
||||
return SCHEMA + uri;
|
||||
}
|
||||
|
||||
function hash(datas: (string | Uint8Array)[]): Uint8Array {
|
||||
const hasher = new sha256.Hash();
|
||||
datas.forEach((data) => {
|
||||
if (typeof data == "string") {
|
||||
data = new TextEncoder().encode(data);
|
||||
}
|
||||
return hasher.update(data);
|
||||
});
|
||||
return hasher.digest();
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
import Connection from "./connection";
|
||||
import _sodium from "libsodium-wrappers";
|
||||
import { CursorData } from "./message";
|
||||
import { loadVp9 } from "./codec";
|
||||
import { checkIfRetry, version } from "./gen_js_from_hbb";
|
||||
import { initZstd, translate } from "./common";
|
||||
import PCMPlayer from "pcm-player";
|
||||
|
||||
window.curConn = undefined;
|
||||
window.isMobile = () => {
|
||||
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
||||
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
|
||||
}
|
||||
|
||||
export function isDesktop() {
|
||||
return !isMobile();
|
||||
}
|
||||
|
||||
export function msgbox(type, title, text) {
|
||||
if (!type || (type == 'error' && !text)) return;
|
||||
const text2 = text.toLowerCase();
|
||||
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
|
||||
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, hasRetry }));
|
||||
}
|
||||
|
||||
function jsonfyForDart(payload) {
|
||||
var tmp = {};
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
if (!key) continue;
|
||||
tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
export function pushEvent(name, payload) {
|
||||
payload = jsonfyForDart(payload);
|
||||
payload.name = name;
|
||||
onGlobalEvent(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
let yuvWorker;
|
||||
let yuvCanvas;
|
||||
let gl;
|
||||
let pixels;
|
||||
let flipPixels;
|
||||
let oldSize;
|
||||
if (YUVCanvas.WebGLFrameSink.isAvailable()) {
|
||||
var canvas = document.createElement('canvas');
|
||||
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
|
||||
gl = canvas.getContext("webgl");
|
||||
} else {
|
||||
yuvWorker = new Worker("./yuv.js");
|
||||
}
|
||||
let testSpeed = [0, 0];
|
||||
|
||||
export function draw(frame) {
|
||||
if (yuvWorker) {
|
||||
// frame's (y/u/v).bytes already detached, can not transferrable any more.
|
||||
yuvWorker.postMessage(frame);
|
||||
} else {
|
||||
var tm0 = new Date().getTime();
|
||||
yuvCanvas.drawFrame(frame);
|
||||
var width = canvas.width;
|
||||
var height = canvas.height;
|
||||
var size = width * height * 4;
|
||||
if (size != oldSize) {
|
||||
pixels = new Uint8Array(size);
|
||||
flipPixels = new Uint8Array(size);
|
||||
oldSize = size;
|
||||
}
|
||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
const row = width * 4;
|
||||
const end = (height - 1) * row;
|
||||
for (let i = 0; i < size; i += row) {
|
||||
flipPixels.set(pixels.subarray(i, i + row), end - i);
|
||||
}
|
||||
onRgba(flipPixels);
|
||||
testSpeed[1] += new Date().getTime() - tm0;
|
||||
testSpeed[0] += 1;
|
||||
if (testSpeed[0] > 30) {
|
||||
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
||||
testSpeed = [0, 0];
|
||||
}
|
||||
}
|
||||
/*
|
||||
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
|
||||
if (testCanvas && currentFrame) {
|
||||
var ctx = testCanvas.getContext("2d");
|
||||
testCanvas.width = frame.format.displayWidth;
|
||||
testCanvas.height = frame.format.displayHeight;
|
||||
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
|
||||
img.data.set(currentFrame);
|
||||
ctx.putImageData(img, 0, 0);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
export function sendOffCanvas(c) {
|
||||
let canvas = c.transferControlToOffscreen();
|
||||
yuvWorker.postMessage({ canvas }, [canvas]);
|
||||
}
|
||||
|
||||
export function setConn(conn) {
|
||||
window.curConn = conn;
|
||||
}
|
||||
|
||||
export function getConn() {
|
||||
return window.curConn;
|
||||
}
|
||||
|
||||
export async function startConn(id) {
|
||||
setByName('remote_id', id);
|
||||
await curConn.start(id);
|
||||
}
|
||||
|
||||
export function close() {
|
||||
getConn()?.close();
|
||||
setConn(undefined);
|
||||
}
|
||||
|
||||
export function newConn() {
|
||||
window.curConn?.close();
|
||||
const conn = new Connection();
|
||||
setConn(conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
let sodium;
|
||||
export async function verify(signed, pk) {
|
||||
if (!sodium) {
|
||||
await _sodium.ready;
|
||||
sodium = _sodium;
|
||||
}
|
||||
if (typeof pk == 'string') {
|
||||
pk = decodeBase64(pk);
|
||||
}
|
||||
return sodium.crypto_sign_open(signed, pk);
|
||||
}
|
||||
|
||||
export function decodeBase64(pk) {
|
||||
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
|
||||
}
|
||||
|
||||
export function genBoxKeyPair() {
|
||||
const pair = sodium.crypto_box_keypair();
|
||||
const sk = pair.privateKey;
|
||||
const pk = pair.publicKey;
|
||||
return [sk, pk];
|
||||
}
|
||||
|
||||
export function genSecretKey() {
|
||||
return sodium.crypto_secretbox_keygen();
|
||||
}
|
||||
|
||||
export function seal(unsigned, theirPk, ourSk) {
|
||||
const nonce = Uint8Array.from(Array(24).fill(0));
|
||||
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
|
||||
}
|
||||
|
||||
function makeOnce(value) {
|
||||
var byteArray = Array(24).fill(0);
|
||||
|
||||
for (var index = 0; index < byteArray.length && value > 0; index++) {
|
||||
var byte = value & 0xff;
|
||||
byteArray[index] = byte;
|
||||
value = (value - byte) / 256;
|
||||
}
|
||||
|
||||
return Uint8Array.from(byteArray);
|
||||
};
|
||||
|
||||
export function encrypt(unsigned, nonce, key) {
|
||||
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
|
||||
}
|
||||
|
||||
export function decrypt(signed, nonce, key) {
|
||||
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
|
||||
}
|
||||
|
||||
window.setByName = (name, value) => {
|
||||
switch (name) {
|
||||
case 'remote_id':
|
||||
localStorage.setItem('remote-id', value);
|
||||
break;
|
||||
case 'connect':
|
||||
newConn();
|
||||
startConn(value);
|
||||
break;
|
||||
case 'login':
|
||||
value = JSON.parse(value);
|
||||
curConn.setRemember(value.remember == 'true');
|
||||
curConn.login(value.password);
|
||||
break;
|
||||
case 'close':
|
||||
close();
|
||||
break;
|
||||
case 'refresh':
|
||||
curConn.refresh();
|
||||
break;
|
||||
case 'reconnect':
|
||||
curConn.reconnect();
|
||||
break;
|
||||
case 'toggle_option':
|
||||
curConn.toggleOption(value);
|
||||
break;
|
||||
case 'image_quality':
|
||||
curConn.setImageQuality(value);
|
||||
break;
|
||||
case 'lock_screen':
|
||||
curConn.lockScreen();
|
||||
break;
|
||||
case 'ctrl_alt_del':
|
||||
curConn.ctrlAltDel();
|
||||
break;
|
||||
case 'switch_display':
|
||||
curConn.switchDisplay(value);
|
||||
break;
|
||||
case 'remove':
|
||||
const peers = getPeers();
|
||||
delete peers[value];
|
||||
localStorage.setItem('peers', JSON.stringify(peers));
|
||||
break;
|
||||
case 'input_key':
|
||||
value = JSON.parse(value);
|
||||
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
||||
break;
|
||||
case 'input_string':
|
||||
curConn.inputString(value);
|
||||
break;
|
||||
case 'send_mouse':
|
||||
let mask = 0;
|
||||
value = JSON.parse(value);
|
||||
switch (value.type) {
|
||||
case 'down':
|
||||
mask = 1;
|
||||
break;
|
||||
case 'up':
|
||||
mask = 2;
|
||||
break;
|
||||
case 'wheel':
|
||||
mask = 3;
|
||||
break;
|
||||
}
|
||||
switch (value.buttons) {
|
||||
case 'left':
|
||||
mask |= 1 << 3;
|
||||
break;
|
||||
case 'right':
|
||||
mask |= 2 << 3;
|
||||
break;
|
||||
case 'wheel':
|
||||
mask |= 4 << 3;
|
||||
}
|
||||
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
|
||||
break;
|
||||
case 'option':
|
||||
value = JSON.parse(value);
|
||||
localStorage.setItem(value.name, value.value);
|
||||
break;
|
||||
case 'peer_option':
|
||||
value = JSON.parse(value);
|
||||
curConn.setOption(value.name, value.value);
|
||||
break;
|
||||
case 'input_os_password':
|
||||
curConn.inputOsPassword(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.getByName = (name, arg) => {
|
||||
let v = _getByName(name, arg);
|
||||
if (typeof v == 'string' || v instanceof String) return v;
|
||||
if (v == undefined || v == null) return '';
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
|
||||
function getPeersForDart() {
|
||||
const peers = [];
|
||||
for (const [id, value] of Object.entries(getPeers())) {
|
||||
if (!id) continue;
|
||||
const tm = value['tm'];
|
||||
const info = value['info'];
|
||||
if (!tm || !info) continue;
|
||||
peers.push([tm, id, info]);
|
||||
}
|
||||
return peers.sort().reverse().map(x => x.slice(1));
|
||||
}
|
||||
|
||||
function _getByName(name, arg) {
|
||||
switch (name) {
|
||||
case 'peers':
|
||||
return getPeersForDart();
|
||||
case 'remote_id':
|
||||
return localStorage.getItem('remote-id');
|
||||
case 'remember':
|
||||
return curConn.getRemember();
|
||||
case 'toggle_option':
|
||||
return curConn.getOption(arg) || false;
|
||||
case 'option':
|
||||
return localStorage.getItem(arg);
|
||||
case 'image_quality':
|
||||
return curConn.getImageQuality();
|
||||
case 'translate':
|
||||
arg = JSON.parse(arg);
|
||||
return translate(arg.locale, arg.text);
|
||||
case 'peer_option':
|
||||
return curConn.getOption(arg);
|
||||
case 'test_if_valid_server':
|
||||
break;
|
||||
case 'version':
|
||||
return version;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
let opusWorker = new Worker("./libopus.js");
|
||||
let pcmPlayer;
|
||||
|
||||
export function initAudio(channels, sampleRate) {
|
||||
pcmPlayer = newAudioPlayer(channels, sampleRate);
|
||||
opusWorker.postMessage({ channels, sampleRate });
|
||||
}
|
||||
|
||||
export function playAudio(packet) {
|
||||
opusWorker.postMessage(packet, [packet.buffer]);
|
||||
}
|
||||
|
||||
window.init = async () => {
|
||||
if (yuvWorker) {
|
||||
yuvWorker.onmessage = (e) => {
|
||||
onRgba(e.data);
|
||||
}
|
||||
}
|
||||
opusWorker.onmessage = (e) => {
|
||||
pcmPlayer.feed(e.data);
|
||||
}
|
||||
loadVp9(() => { });
|
||||
await initZstd();
|
||||
console.log('init done');
|
||||
}
|
||||
|
||||
export function getPeers() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('peers')) || {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function newAudioPlayer(channels, sampleRate) {
|
||||
return new PCMPlayer({
|
||||
channels,
|
||||
sampleRate,
|
||||
flushingTime: 2000
|
||||
});
|
||||
}
|
||||
|
||||
export function copyToClipboard(text) {
|
||||
if (window.clipboardData && window.clipboardData.setData) {
|
||||
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
|
||||
return window.clipboardData.setData("Text", text);
|
||||
|
||||
}
|
||||
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
|
||||
var textarea = document.createElement("textarea");
|
||||
textarea.textContent = text;
|
||||
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
|
||||
}
|
||||
catch (ex) {
|
||||
console.warn("Copy to clipboard failed.", ex);
|
||||
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
|
||||
}
|
||||
finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import "./globals";
|
||||
import "./ui";
|
||||
@@ -1,8 +0,0 @@
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import "./style.css";
|
||||
import "./connection";
|
||||
import * as globals from "./globals";
|
||||
|
||||
const app = document.querySelector('#app');
|
||||
|
||||
if (app) {
|
||||
app.innerHTML = `
|
||||
<div id="connect" style="text-align: center"><table style="display: inline-block">
|
||||
<tr><td><span>Host: </span></td><td><input id="host" /></td></tr>
|
||||
<tr><td><span>Key: </span></td><td><input id="key" /></td></tr>
|
||||
<tr><td><span>Id: </span></td><td><input id="id" /></td></tr>
|
||||
<tr><td></td><td><button onclick="connect();">Connect</button></td></tr>
|
||||
</table></div>
|
||||
<div id="password" style="display: none;">
|
||||
<input type="password" id="password" />
|
||||
<button id="confirm" onclick="confirm()">Confirm</button>
|
||||
<button id="cancel" onclick="cancel();">Cancel</button>
|
||||
</div>
|
||||
<div id="status" style="display: none;">
|
||||
<div id="text" style="line-height: 2em"></div>
|
||||
<button id="cancel" onclick="cancel();">Cancel</button>
|
||||
</div>
|
||||
<div id="canvas" style="display: none;">
|
||||
<button id="cancel" onclick="cancel();">Cancel</button>
|
||||
<canvas id="player"></canvas>
|
||||
<canvas id="test-yuv-decoder-canvas"></canvas>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let player;
|
||||
window.init();
|
||||
|
||||
document.body.onload = () => {
|
||||
const host = document.querySelector('#host');
|
||||
host.value = localStorage.getItem('custom-rendezvous-server');
|
||||
const id = document.querySelector('#id');
|
||||
id.value = localStorage.getItem('id');
|
||||
const key = document.querySelector('#key');
|
||||
key.value = localStorage.getItem('key');
|
||||
player = YUVCanvas.attach(document.getElementById('player'));
|
||||
// globals.sendOffCanvas(document.getElementById('player'));
|
||||
};
|
||||
|
||||
window.connect = () => {
|
||||
const host = document.querySelector('#host');
|
||||
localStorage.setItem('custom-rendezvous-server', host.value);
|
||||
const id = document.querySelector('#id');
|
||||
localStorage.setItem('id', id.value);
|
||||
const key = document.querySelector('#key');
|
||||
localStorage.setItem('key', key.value);
|
||||
const func = async () => {
|
||||
const conn = globals.newConn();
|
||||
conn.setMsgbox(msgbox);
|
||||
conn.setDraw((f) => {
|
||||
/*
|
||||
if (!(document.getElementById('player').width > 0)) {
|
||||
document.getElementById('player').width = f.format.displayWidth;
|
||||
document.getElementById('player').height = f.format.displayHeight;
|
||||
}
|
||||
*/
|
||||
globals.draw(f);
|
||||
player.drawFrame(f);
|
||||
});
|
||||
document.querySelector('div#status').style.display = 'block';
|
||||
document.querySelector('div#connect').style.display = 'none';
|
||||
document.querySelector('div#text').innerHTML = 'Connecting ...';
|
||||
await conn.start(id.value);
|
||||
};
|
||||
func();
|
||||
}
|
||||
|
||||
function msgbox(type, title, text) {
|
||||
if (!globals.getConn()) return;
|
||||
if (type == 'input-password') {
|
||||
document.querySelector('div#status').style.display = 'none';
|
||||
document.querySelector('div#password').style.display = 'block';
|
||||
} else if (!type) {
|
||||
document.querySelector('div#canvas').style.display = 'block';
|
||||
document.querySelector('div#password').style.display = 'none';
|
||||
document.querySelector('div#status').style.display = 'none';
|
||||
} else if (type == 'error') {
|
||||
document.querySelector('div#status').style.display = 'block';
|
||||
document.querySelector('div#canvas').style.display = 'none';
|
||||
document.querySelector('div#text').innerHTML = '<div style="color: red; font-weight: bold;">' + text + '</div>';
|
||||
} else {
|
||||
document.querySelector('div#password').style.display = 'none';
|
||||
document.querySelector('div#status').style.display = 'block';
|
||||
document.querySelector('div#text').innerHTML = '<div style="font-weight: bold;">' + text + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
window.cancel = () => {
|
||||
globals.close();
|
||||
document.querySelector('div#connect').style.display = 'block';
|
||||
document.querySelector('div#password').style.display = 'none';
|
||||
document.querySelector('div#status').style.display = 'none';
|
||||
document.querySelector('div#canvas').style.display = 'none';
|
||||
}
|
||||
|
||||
window.confirm = () => {
|
||||
const password = document.querySelector('input#password').value;
|
||||
if (password) {
|
||||
document.querySelector('div#password').style.display = 'none';
|
||||
globals.getConn().login(password);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,183 +0,0 @@
|
||||
import * as message from "./message.js";
|
||||
import * as rendezvous from "./rendezvous.js";
|
||||
import * as globals from "./globals";
|
||||
|
||||
type Keys = "message" | "open" | "close" | "error";
|
||||
|
||||
export default class Websock {
|
||||
_websocket: WebSocket;
|
||||
_eventHandlers: { [key in Keys]: Function };
|
||||
_buf: (rendezvous.RendezvousMessage | message.Message)[];
|
||||
_status: any;
|
||||
_latency: number;
|
||||
_secretKey: [Uint8Array, number, number] | undefined;
|
||||
_uri: string;
|
||||
_isRendezvous: boolean;
|
||||
|
||||
constructor(uri: string, isRendezvous: boolean = true) {
|
||||
this._eventHandlers = {
|
||||
message: (_: any) => {},
|
||||
open: () => {},
|
||||
close: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
this._uri = uri;
|
||||
this._status = "";
|
||||
this._buf = [];
|
||||
this._websocket = new WebSocket(uri);
|
||||
this._websocket.onmessage = this._recv_message.bind(this);
|
||||
this._websocket.binaryType = "arraybuffer";
|
||||
this._latency = new Date().getTime();
|
||||
this._isRendezvous = isRendezvous;
|
||||
}
|
||||
|
||||
latency(): number {
|
||||
return this._latency;
|
||||
}
|
||||
|
||||
setSecretKey(key: Uint8Array) {
|
||||
this._secretKey = [key, 0, 0];
|
||||
}
|
||||
|
||||
sendMessage(json: message.DeepPartial<message.Message>) {
|
||||
let data = message.Message.encode(
|
||||
message.Message.fromPartial(json)
|
||||
).finish();
|
||||
let k = this._secretKey;
|
||||
if (k) {
|
||||
k[1] += 1;
|
||||
data = globals.encrypt(data, k[1], k[0]);
|
||||
}
|
||||
this._websocket.send(data);
|
||||
}
|
||||
|
||||
sendRendezvous(data: rendezvous.DeepPartial<rendezvous.RendezvousMessage>) {
|
||||
this._websocket.send(
|
||||
rendezvous.RendezvousMessage.encode(
|
||||
rendezvous.RendezvousMessage.fromPartial(data)
|
||||
).finish()
|
||||
);
|
||||
}
|
||||
|
||||
parseMessage(data: Uint8Array) {
|
||||
return message.Message.decode(data);
|
||||
}
|
||||
|
||||
parseRendezvous(data: Uint8Array) {
|
||||
return rendezvous.RendezvousMessage.decode(data);
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
off(evt: Keys) {
|
||||
this._eventHandlers[evt] = () => {};
|
||||
}
|
||||
|
||||
on(evt: Keys, handler: Function) {
|
||||
this._eventHandlers[evt] = handler;
|
||||
}
|
||||
|
||||
async open(timeout: number = 12000): Promise<Websock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (this._status != "open") {
|
||||
reject(this._status || "Timeout");
|
||||
}
|
||||
}, timeout);
|
||||
this._websocket.onopen = () => {
|
||||
this._latency = new Date().getTime() - this._latency;
|
||||
this._status = "open";
|
||||
console.debug(">> WebSock.onopen");
|
||||
if (this._websocket?.protocol) {
|
||||
console.info(
|
||||
"Server choose sub-protocol: " + this._websocket.protocol
|
||||
);
|
||||
}
|
||||
|
||||
this._eventHandlers.open();
|
||||
console.info("WebSock.onopen");
|
||||
resolve(this);
|
||||
};
|
||||
this._websocket.onclose = (e) => {
|
||||
if (this._status == "open") {
|
||||
// e.code 1000 means that the connection was closed normally.
|
||||
//
|
||||
}
|
||||
this._status = e;
|
||||
console.error("WebSock.onclose: ");
|
||||
console.error(e);
|
||||
this._eventHandlers.close(e);
|
||||
reject("Reset by the peer");
|
||||
};
|
||||
this._websocket.onerror = (e: any) => {
|
||||
if (!this._status) {
|
||||
reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server");
|
||||
return;
|
||||
}
|
||||
this._status = e;
|
||||
console.error("WebSock.onerror: ")
|
||||
console.error(e);
|
||||
this._eventHandlers.error(e);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async next(
|
||||
timeout = 12000
|
||||
): Promise<rendezvous.RendezvousMessage | message.Message> {
|
||||
const func = (
|
||||
resolve: (value: rendezvous.RendezvousMessage | message.Message) => void,
|
||||
reject: (reason: any) => void,
|
||||
tm0: number
|
||||
) => {
|
||||
if (this._buf.length) {
|
||||
resolve(this._buf[0]);
|
||||
this._buf.splice(0, 1);
|
||||
} else {
|
||||
if (this._status != "open") {
|
||||
reject(this._status);
|
||||
return;
|
||||
}
|
||||
if (new Date().getTime() > tm0 + timeout) {
|
||||
reject("Timeout");
|
||||
} else {
|
||||
setTimeout(() => func(resolve, reject, tm0), 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
func(resolve, reject, new Date().getTime());
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this._status = "";
|
||||
if (this._websocket) {
|
||||
if (
|
||||
this._websocket.readyState === WebSocket.OPEN ||
|
||||
this._websocket.readyState === WebSocket.CONNECTING
|
||||
) {
|
||||
console.info("Closing WebSocket connection");
|
||||
this._websocket.close();
|
||||
}
|
||||
|
||||
this._websocket.onmessage = () => {};
|
||||
}
|
||||
}
|
||||
|
||||
_recv_message(e: any) {
|
||||
if (e.data instanceof window.ArrayBuffer) {
|
||||
let bytes = new Uint8Array(e.data);
|
||||
const k = this._secretKey;
|
||||
if (k) {
|
||||
k[2] += 1;
|
||||
bytes = globals.decrypt(bytes, k[2], k[0]);
|
||||
}
|
||||
this._buf.push(
|
||||
this._isRendezvous
|
||||
? this.parseRendezvous(bytes)
|
||||
: this.parseMessage(bytes)
|
||||
);
|
||||
}
|
||||
this._eventHandlers.message(e.data);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
path = os.path.abspath(os.path.join(os.getcwd(), '..', '..', '..', 'libs', 'hbb_common', 'protos'))
|
||||
|
||||
if os.name == 'nt':
|
||||
cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ message.proto'%path
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
else:
|
||||
cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ message.proto'%path
|
||||
print(cmd)
|
||||
os.system(cmd)
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
manifest: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `[name].js`,
|
||||
chunkFileNames: `[name].js`,
|
||||
assetFileNames: `[name].[ext]`,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,2 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("@firebase/app")):"function"==typeof define&&define.amd?define(["@firebase/app"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).firebase)}(this,function(mt){"use strict";try{!function(){function e(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=e(mt),r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var s=function(){return(s=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var i in t=arguments[n])Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i]);return e}).apply(this,arguments)};function p(e,a,s,u){return new(s=s||Promise)(function(n,t){function r(e){try{o(u.next(e))}catch(e){t(e)}}function i(e){try{o(u.throw(e))}catch(e){t(e)}}function o(e){var t;e.done?n(e.value):((t=e.value)instanceof s?t:new s(function(e){e(t)})).then(r,i)}o((u=u.apply(e,a||[])).next())})}function h(n,r){var i,o,a,s={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},e={next:t(0),throw:t(1),return:t(2)};return"function"==typeof Symbol&&(e[Symbol.iterator]=function(){return this}),e;function t(t){return function(e){return function(t){if(i)throw new TypeError("Generator is already executing.");for(;s;)try{if(i=1,o&&(a=2&t[0]?o.return:t[0]?o.throw||((a=o.return)&&a.call(o),0):o.next)&&!(a=a.call(o,t[1])).done)return a;switch(o=0,(t=a?[2&t[0],a.value]:t)[0]){case 0:case 1:a=t;break;case 4:return s.label++,{value:t[1],done:!1};case 5:s.label++,o=t[1],t=[0];continue;case 7:t=s.ops.pop(),s.trys.pop();continue;default:if(!(a=0<(a=s.trys).length&&a[a.length-1])&&(6===t[0]||2===t[0])){s=0;continue}if(3===t[0]&&(!a||t[1]>a[0]&&t[1]<a[3])){s.label=t[1];break}if(6===t[0]&&s.label<a[1]){s.label=a[1],a=t;break}if(a&&s.label<a[2]){s.label=a[2],s.ops.push(t);break}a[2]&&s.ops.pop(),s.trys.pop();continue}t=r.call(n,s)}catch(e){t=[6,e],o=0}finally{i=a=0}if(5&t[0])throw t[1];return{value:t[0]?t[1]:void 0,done:!0}}([t,e])}}}function a(e){var t="function"==typeof Symbol&&Symbol.iterator,n=t&&e[t],r=0;if(n)return n.call(e);if(e&&"number"==typeof e.length)return{next:function(){return{value:(e=e&&r>=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function u(e,t){for(var n=0,r=t.length,i=e.length;n<r;n++,i++)e[i]=t[n];return e}function c(){var e="object"==typeof chrome?chrome.runtime:"object"==typeof browser?browser.runtime:void 0;return"object"==typeof e&&void 0!==e.id}function v(){return"indexedDB"in self&&null!=indexedDB}function g(){return new Promise(function(e,t){try{var n=!0,r="validate-browser-context-for-indexeddb-analytics-module",i=self.indexedDB.open(r);i.onsuccess=function(){i.result.close(),n||self.indexedDB.deleteDatabase(r),e(!0)},i.onupgradeneeded=function(){n=!1},i.onerror=function(){var e;t((null===(e=i.error)||void 0===e?void 0:e.message)||"")}}catch(e){t(e)}})}function l(){return!(!navigator||!navigator.cookieEnabled)}var i,o="FirebaseError",f=(function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}(d,i=Error),d);function d(e,t,n){t=i.call(this,t)||this;return t.code=e,t.customData=n,t.name=o,Object.setPrototypeOf(t,d.prototype),Error.captureStackTrace&&Error.captureStackTrace(t,m.prototype.create),t}var m=(n.prototype.create=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r,i=t[0]||{},o=this.service+"/"+e,e=this.errors[e],e=e?(r=i,e.replace(y,function(e,t){var n=r[t];return null!=n?String(n):"<"+t+"?>"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new f(o,e,i)},n);function n(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var y=/\{\$([^}]+)}/g,b=1e3,w=2,I=144e5,_=.5;function E(e,t,n){void 0===n&&(n=w);n=(t=void 0===t?b:t)*Math.pow(n,e),e=Math.round(_*n*(Math.random()-.5)*2);return Math.min(I,n+e)}var T=(S.prototype.setInstantiationMode=function(e){return this.instantiationMode=e,this},S.prototype.setMultipleInstances=function(e){return this.multipleInstances=e,this},S.prototype.setServiceProps=function(e){return this.serviceProps=e,this},S.prototype.setInstanceCreatedCallback=function(e){return this.onInstanceCreated=e,this},S);function S(e,t,n){this.name=e,this.instanceFactory=t,this.type=n,this.multipleInstances=!1,this.serviceProps={},this.instantiationMode="LAZY",this.onInstanceCreated=null}function C(n){return new Promise(function(e,t){n.onsuccess=function(){e(n.result)},n.onerror=function(){t(n.error)}})}function O(n,r,i){var o,e=new Promise(function(e,t){C(o=n[r].apply(n,i)).then(e,t)});return e.request=o,e}function N(e,n,t){t.forEach(function(t){Object.defineProperty(e.prototype,t,{get:function(){return this[n][t]},set:functLine truncated
|
||||
//# sourceMappingURL=firebase-analytics.js.map
|
||||
@@ -1,2 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).firebase=t()}(this,function(){"use strict";var r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var n=function(){return(n=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var i in t=arguments[n])Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i]);return e}).apply(this,arguments)};function e(e,a,s,l){return new(s=s||Promise)(function(n,t){function r(e){try{o(l.next(e))}catch(e){t(e)}}function i(e){try{o(l.throw(e))}catch(e){t(e)}}function o(e){var t;e.done?n(e.value):((t=e.value)instanceof s?t:new s(function(e){e(t)})).then(r,i)}o((l=l.apply(e,a||[])).next())})}function i(n,r){var i,o,a,s={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},e={next:t(0),throw:t(1),return:t(2)};return"function"==typeof Symbol&&(e[Symbol.iterator]=function(){return this}),e;function t(t){return function(e){return function(t){if(i)throw new TypeError("Generator is already executing.");for(;s;)try{if(i=1,o&&(a=2&t[0]?o.return:t[0]?o.throw||((a=o.return)&&a.call(o),0):o.next)&&!(a=a.call(o,t[1])).done)return a;switch(o=0,(t=a?[2&t[0],a.value]:t)[0]){case 0:case 1:a=t;break;case 4:return s.label++,{value:t[1],done:!1};case 5:s.label++,o=t[1],t=[0];continue;case 7:t=s.ops.pop(),s.trys.pop();continue;default:if(!(a=0<(a=s.trys).length&&a[a.length-1])&&(6===t[0]||2===t[0])){s=0;continue}if(3===t[0]&&(!a||t[1]>a[0]&&t[1]<a[3])){s.label=t[1];break}if(6===t[0]&&s.label<a[1]){s.label=a[1],a=t;break}if(a&&s.label<a[2]){s.label=a[2],s.ops.push(t);break}a[2]&&s.ops.pop(),s.trys.pop();continue}t=r.call(n,s)}catch(e){t=[6,e],o=0}finally{i=a=0}if(5&t[0])throw t[1];return{value:t[0]?t[1]:void 0,done:!0}}([t,e])}}}function p(e){var t="function"==typeof Symbol&&Symbol.iterator,n=t&&e[t],r=0;if(n)return n.call(e);if(e&&"number"==typeof e.length)return{next:function(){return{value:(e=e&&r>=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0<t--)&&!(r=o.next()).done;)a.push(r.value)}catch(e){i={error:e}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function a(e,t){for(var n=0,r=t.length,i=e.length;n<r;n++,i++)e[i]=t[n];return e}function h(e,t){if(!(t instanceof Object))return t;switch(t.constructor){case Date:return new Date(t.getTime());case Object:void 0===e&&(e={});break;case Array:e=[];break;default:return t}for(var n in t)t.hasOwnProperty(n)&&"__proto__"!==n&&(e[n]=h(e[n],t[n]));return e}var o=(t.prototype.wrapCallback=function(n){var r=this;return function(e,t){e?r.reject(e):r.resolve(t),"function"==typeof n&&(r.promise.catch(function(){}),1===n.length?n(e):n(e,t))}},t);function t(){var n=this;this.reject=function(){},this.resolve=function(){},this.promise=new Promise(function(e,t){n.resolve=e,n.reject=t})}var s,l="FirebaseError",c=(function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}(u,s=Error),u);function u(e,t,n){t=s.call(this,t)||this;return t.code=e,t.customData=n,t.name=l,Object.setPrototypeOf(t,u.prototype),Error.captureStackTrace&&Error.captureStackTrace(t,d.prototype.create),t}var d=(v.prototype.create=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r,i=t[0]||{},o=this.service+"/"+e,e=this.errors[e],e=e?(r=i,e.replace(m,function(e,t){var n=r[t];return null!=n?String(n):"<"+t+"?>"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new c(o,e,i)},v);function v(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var m=/\{\$([^}]+)}/g;function y(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function g(e,t){t=new b(e,t);return t.subscribe.bind(t)}var b=(I.prototype.next=function(t){this.forEachObserver(function(e){e.next(t)})},I.prototype.error=function(t){this.forEachObserver(function(e){e.error(t)}),this.close(t)},I.prototype.complete=function(){this.forEachObserver(function(e){e.complete()}),this.close()},I.prototype.subscribe=function(e,t,n){var r,i=this;if(void 0===e&&void 0===t&&void 0===n)throw new Error("Missing Observer.");void 0===(r=function(e,t){if("object"!=typeof e||null===e)return!1;for(var n=0,r=t;n<r.length;n++){var i=r[n];if(i in e&&"function"==typeof e[i])return!0}return!1}(e,["next","error","complete"])?e:{next:e,error:t,complete:n}).next&&(r.next=w),void 0===r.error&&(r.error=w),void 0===r.complete&&(r.complete=w);n=this.unsubscribeOne.bind(this,this.observers.length);reLine truncated
|
||||
//# sourceMappingURL=firebase-app.js.map
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "rustdesk",
|
||||
"short_name": "rustdesk",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "Remote Desktop.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
var wasmExports;
|
||||
|
||||
fetch('yuv.wasm').then(function (res) { return res.arrayBuffer(); })
|
||||
.then(function (file) { return WebAssembly.instantiate(file); })
|
||||
.then(function (wasm) {
|
||||
wasmExports = wasm.instance.exports;
|
||||
console.log('yuv ready');
|
||||
});
|
||||
|
||||
var yPtr, yPtrLen, uPtr, uPtrLen, vPtr, vPtrLen, outPtr, outPtrLen;
|
||||
let testSpeed = [0, 0];
|
||||
function I420ToARGB(yb) {
|
||||
if (!wasmExports) return;
|
||||
var tm0 = new Date().getTime();
|
||||
var { malloc, free, memory } = wasmExports;
|
||||
var HEAPU8 = new Uint8Array(memory.buffer);
|
||||
let n = yb.y.bytes.length;
|
||||
if (yPtrLen != n) {
|
||||
if (yPtr) free(yPtr);
|
||||
yPtrLen = n;
|
||||
yPtr = malloc(n);
|
||||
}
|
||||
HEAPU8.set(yb.y.bytes, yPtr);
|
||||
n = yb.u.bytes.length;
|
||||
if (uPtrLen != n) {
|
||||
if (uPtr) free(uPtr);
|
||||
uPtrLen = n;
|
||||
uPtr = malloc(n);
|
||||
}
|
||||
HEAPU8.set(yb.u.bytes, uPtr);
|
||||
n = yb.v.bytes.length;
|
||||
if (vPtrLen != n) {
|
||||
if (vPtr) free(vPtr);
|
||||
vPtrLen = n;
|
||||
vPtr = malloc(n);
|
||||
}
|
||||
HEAPU8.set(yb.v.bytes, vPtr);
|
||||
var w = yb.format.displayWidth;
|
||||
var h = yb.format.displayHeight;
|
||||
n = w * h * 4;
|
||||
if (outPtrLen != n) {
|
||||
if (outPtr) free(outPtr);
|
||||
outPtrLen = n;
|
||||
outPtr = malloc(n);
|
||||
HEAPU8.fill(255, outPtr, outPtr + n);
|
||||
}
|
||||
// var res = wasmExports.I420ToARGB(yPtr, yb.y.stride, uPtr, yb.u.stride, vPtr, yb.v.stride, outPtr, w * 4, w, h);
|
||||
// var res = wasmExports.AVX_YUV_to_ARGB(outPtr, yPtr, yb.y.stride, uPtr, yb.u.stride, vPtr, yb.v.stride, w, h);
|
||||
var res = wasmExports.yuv420_rgb24_std(w, h, yPtr, uPtr, vPtr, yb.y.stride, yb.v.stride, outPtr, w * 4, 1);
|
||||
var out = HEAPU8.slice(outPtr, outPtr + n);
|
||||
testSpeed[1] += new Date().getTime() - tm0;
|
||||
testSpeed[0] += 1;
|
||||
if (testSpeed[0] > 30) {
|
||||
console.log('yuv: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
||||
testSpeed = [0, 0];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
var currentFrame;
|
||||
self.addEventListener('message', (e) => {
|
||||
currentFrame = e.data;
|
||||
});
|
||||
|
||||
function run() {
|
||||
if (currentFrame) {
|
||||
self.postMessage(I420ToARGB(currentFrame));
|
||||
currentFrame = undefined;
|
||||
}
|
||||
setTimeout(run, 1);
|
||||
}
|
||||
|
||||
run();
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
Under dev.
|
||||
+1
-1
Submodule libs/hbb_common updated: 117ea7c341...f850a167ac.
+35
-13
@@ -47,8 +47,8 @@ use hbb_common::{
|
||||
anyhow::{anyhow, Context},
|
||||
bail,
|
||||
config::{
|
||||
self, use_ws, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT,
|
||||
READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS,
|
||||
self, keys, use_ws, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution,
|
||||
CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS,
|
||||
},
|
||||
fs::JobType,
|
||||
futures::future::{select_ok, FutureExt},
|
||||
@@ -274,7 +274,6 @@ impl Client {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if crate::get_ipv6_punch_enabled() {
|
||||
crate::test_ipv6().await;
|
||||
}
|
||||
@@ -1784,6 +1783,9 @@ impl LoginConfigHandler {
|
||||
/// * `v` - value of option
|
||||
pub fn set_option(&mut self, k: String, v: String) {
|
||||
let mut config = self.load_config();
|
||||
if v == self.get_option(&k) {
|
||||
return;
|
||||
}
|
||||
config.options.insert(k, v);
|
||||
self.save_config(config);
|
||||
}
|
||||
@@ -1952,6 +1954,14 @@ impl LoginConfigHandler {
|
||||
BoolOption::No
|
||||
})
|
||||
.into();
|
||||
} else if name == keys::OPTION_TERMINAL_PERSISTENT {
|
||||
config.terminal_persistent.v = !config.terminal_persistent.v;
|
||||
option.terminal_persistent = (if config.terminal_persistent.v {
|
||||
BoolOption::Yes
|
||||
} else {
|
||||
BoolOption::No
|
||||
})
|
||||
.into();
|
||||
} else if name == "privacy-mode" {
|
||||
// try toggle privacy mode
|
||||
option.privacy_mode = (if config.privacy_mode.v {
|
||||
@@ -2049,6 +2059,14 @@ impl LoginConfigHandler {
|
||||
return None;
|
||||
}
|
||||
let mut msg = OptionMessage::new();
|
||||
if self.conn_type.eq(&ConnType::TERMINAL) {
|
||||
if self.get_toggle_option(keys::OPTION_TERMINAL_PERSISTENT) {
|
||||
msg.terminal_persistent = BoolOption::Yes.into();
|
||||
return Some(msg);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let q = self.image_quality.clone();
|
||||
if let Some(q) = self.get_image_quality_enum(&q, ignore_default) {
|
||||
msg.image_quality = q.into();
|
||||
@@ -2094,7 +2112,7 @@ impl LoginConfigHandler {
|
||||
if self.get_toggle_option("disable-audio") {
|
||||
msg.disable_audio = BoolOption::Yes.into();
|
||||
}
|
||||
if !view_only && self.get_toggle_option(config::keys::OPTION_ENABLE_FILE_COPY_PASTE) {
|
||||
if !view_only && self.get_toggle_option(keys::OPTION_ENABLE_FILE_COPY_PASTE) {
|
||||
msg.enable_file_transfer = BoolOption::Yes.into();
|
||||
}
|
||||
if view_only || self.get_toggle_option("disable-clipboard") {
|
||||
@@ -2150,9 +2168,11 @@ impl LoginConfigHandler {
|
||||
self.config.show_remote_cursor.v
|
||||
} else if name == "lock-after-session-end" {
|
||||
self.config.lock_after_session_end.v
|
||||
} else if name == keys::OPTION_TERMINAL_PERSISTENT {
|
||||
self.config.terminal_persistent.v
|
||||
} else if name == "privacy-mode" {
|
||||
self.config.privacy_mode.v
|
||||
} else if name == config::keys::OPTION_ENABLE_FILE_COPY_PASTE {
|
||||
} else if name == keys::OPTION_ENABLE_FILE_COPY_PASTE {
|
||||
self.config.enable_file_copy_paste.v
|
||||
} else if name == "disable-audio" {
|
||||
self.config.disable_audio.v
|
||||
@@ -2452,7 +2472,7 @@ impl LoginConfigHandler {
|
||||
} else {
|
||||
(my_id, self.id.clone())
|
||||
};
|
||||
let mut display_name = get_builtin_option(config::keys::OPTION_DISPLAY_NAME);
|
||||
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
||||
if display_name.is_empty() {
|
||||
display_name =
|
||||
serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option("user_info"))
|
||||
@@ -2522,6 +2542,11 @@ impl LoginConfigHandler {
|
||||
port: self.port_forward.1,
|
||||
..Default::default()
|
||||
}),
|
||||
ConnType::TERMINAL => {
|
||||
let mut terminal = Terminal::new();
|
||||
terminal.service_id = self.get_option("terminal-service-id");
|
||||
lr.set_terminal(terminal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -3237,8 +3262,7 @@ pub async fn handle_hash(
|
||||
}
|
||||
|
||||
if password.is_empty() {
|
||||
let p =
|
||||
crate::ui_interface::get_builtin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD);
|
||||
let p = crate::ui_interface::get_builtin_option(keys::OPTION_DEFAULT_CONNECT_PASSWORD);
|
||||
if !p.is_empty() {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(p.clone());
|
||||
@@ -3789,11 +3813,9 @@ pub mod peer_online {
|
||||
}
|
||||
// Retry for 2 times to get the online response
|
||||
for _ in 0..2 {
|
||||
if let Some(msg_in) = crate::get_next_nonkeyexchange_msg(
|
||||
&mut socket,
|
||||
Some(timeout.as_millis() as _),
|
||||
)
|
||||
.await
|
||||
if let Some(msg_in) =
|
||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout.as_millis() as _))
|
||||
.await
|
||||
{
|
||||
match msg_in.union {
|
||||
Some(rendezvous_message::Union::OnlineResponse(online_response)) => {
|
||||
|
||||
+54
-19
@@ -85,6 +85,7 @@ struct ParsedPeerInfo {
|
||||
is_installed: bool,
|
||||
idd_impl: String,
|
||||
support_view_camera: bool,
|
||||
support_terminal: bool,
|
||||
}
|
||||
|
||||
impl ParsedPeerInfo {
|
||||
@@ -131,10 +132,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let _file_clip_context_holder = {
|
||||
// `is_port_forward()` will not reach here, but we still check it for clarity.
|
||||
if !self.handler.is_file_transfer()
|
||||
&& !self.handler.is_port_forward()
|
||||
&& !self.handler.is_view_camera()
|
||||
{
|
||||
if self.handler.is_default() {
|
||||
// It is ok to call this function multiple times.
|
||||
ContextSend::enable(true);
|
||||
Some(crate::SimpleCallOnReturn {
|
||||
@@ -159,6 +157,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
ConnType::FILE_TRANSFER
|
||||
} else if self.handler.is_view_camera() {
|
||||
ConnType::VIEW_CAMERA
|
||||
} else if self.handler.is_terminal() {
|
||||
ConnType::TERMINAL
|
||||
} else {
|
||||
ConnType::default()
|
||||
};
|
||||
@@ -195,11 +195,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None);
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
{
|
||||
let is_conn_not_default = self.handler.is_file_transfer()
|
||||
|| self.handler.is_port_forward()
|
||||
|| self.handler.is_rdp()
|
||||
|| self.handler.is_view_camera();
|
||||
if !is_conn_not_default {
|
||||
if self.handler.is_default() {
|
||||
(self.client_conn_id, rx_clip_client_holder.0) =
|
||||
clipboard::get_rx_cliprdr_client(&self.handler.get_id());
|
||||
log::debug!("get cliprdr client for conn_id {}", self.client_conn_id);
|
||||
@@ -338,12 +334,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.set_disconnected(round);
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if !self.handler.is_view_camera() && _set_disconnected_ok {
|
||||
if self.handler.is_default() && _set_disconnected_ok {
|
||||
Client::try_stop_clipboard();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
if !self.handler.is_view_camera() && _set_disconnected_ok {
|
||||
if self.handler.is_default() && _set_disconnected_ok {
|
||||
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
|
||||
}
|
||||
}
|
||||
@@ -437,7 +433,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
|
||||
// Start a voice call recorder, records audio and send to remote
|
||||
fn start_voice_call(&mut self) -> Option<std::sync::mpsc::Sender<()>> {
|
||||
if self.handler.is_file_transfer() || self.handler.is_port_forward() {
|
||||
if self.handler.is_file_transfer()
|
||||
|| self.handler.is_port_forward()
|
||||
|| self.handler.is_terminal()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// iOS does not have this server.
|
||||
@@ -1230,6 +1229,24 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn check_terminal_support(&self, peer_version: &str) -> bool {
|
||||
if self.peer_info.support_terminal {
|
||||
return true;
|
||||
}
|
||||
if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.4.1") {
|
||||
self.handler.msgbox(
|
||||
"error",
|
||||
"Remote terminal not supported",
|
||||
"Remote terminal is not supported by the remote side. Please upgrade to version 1.4.1 or higher.",
|
||||
"",
|
||||
);
|
||||
} else {
|
||||
self.handler
|
||||
.on_error("Remote terminal is not supported by the remote side");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool {
|
||||
if let Ok(msg_in) = Message::parse_from_bytes(&data) {
|
||||
match msg_in.union {
|
||||
@@ -1290,13 +1307,16 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if self.handler.is_terminal() {
|
||||
if !self.check_terminal_support(&peer_version) {
|
||||
self.handler.lc.write().unwrap().handle_peer_info(&pi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.handler.handle_peer_info(pi);
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
self.check_clipboard_file_context();
|
||||
if !(self.handler.is_file_transfer()
|
||||
|| self.handler.is_port_forward()
|
||||
|| self.handler.is_view_camera())
|
||||
{
|
||||
if self.handler.is_default() {
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
let rx = Client::try_start_clipboard(None);
|
||||
@@ -1661,9 +1681,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(Permission::Camera) => {
|
||||
self.handler.set_permission("camera", p.enabled);
|
||||
}
|
||||
Ok(Permission::Restart) => {
|
||||
self.handler.set_permission("restart", p.enabled);
|
||||
}
|
||||
@@ -1923,6 +1940,18 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.handler
|
||||
.handle_screenshot_resp(response.sid, response.msg);
|
||||
}
|
||||
Some(message::Union::TerminalResponse(response)) => {
|
||||
use hbb_common::message_proto::terminal_response::Union;
|
||||
if let Some(Union::Opened(opened)) = &response.union {
|
||||
if opened.success && !opened.service_id.is_empty() {
|
||||
self.handler.lc.write().unwrap().set_option(
|
||||
"terminal-service-id".to_owned(),
|
||||
opened.service_id.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
self.handler.handle_terminal_response(response);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1931,6 +1960,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
|
||||
fn set_peer_info(&mut self, pi: &PeerInfo) {
|
||||
self.peer_info.platform = pi.platform.clone();
|
||||
|
||||
// Check features field for terminal support
|
||||
if let Some(features) = pi.features.as_ref() {
|
||||
self.peer_info.support_terminal = features.terminal;
|
||||
}
|
||||
|
||||
if let Ok(platform_additions) =
|
||||
serde_json::from_str::<HashMap<String, serde_json::Value>>(&pi.platform_additions)
|
||||
{
|
||||
|
||||
@@ -1105,6 +1105,60 @@ impl InvokeUiSession for FlutterHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_terminal_response(&self, response: TerminalResponse) {
|
||||
use hbb_common::message_proto::terminal_response::Union;
|
||||
|
||||
match response.union {
|
||||
Some(Union::Opened(opened)) => {
|
||||
let mut event_data: Vec<(&str, serde_json::Value)> = vec![
|
||||
("type", json!("opened")),
|
||||
("terminal_id", json!(opened.terminal_id)),
|
||||
("success", json!(opened.success)),
|
||||
("message", json!(&opened.message)),
|
||||
("pid", json!(opened.pid)),
|
||||
("service_id", json!(&opened.service_id)),
|
||||
];
|
||||
self.push_event_("terminal_response", &event_data, &[], &[]);
|
||||
}
|
||||
Some(Union::Data(data)) => {
|
||||
// Decompress data if needed
|
||||
let output_data = if data.compressed {
|
||||
hbb_common::compress::decompress(&data.data)
|
||||
} else {
|
||||
data.data.to_vec()
|
||||
};
|
||||
|
||||
let encoded = crate::encode64(&output_data);
|
||||
let event_data: Vec<(&str, serde_json::Value)> = vec![
|
||||
("type", json!("data")),
|
||||
("terminal_id", json!(data.terminal_id)),
|
||||
("data", json!(&encoded)),
|
||||
];
|
||||
self.push_event_("terminal_response", &event_data, &[], &[]);
|
||||
}
|
||||
Some(Union::Closed(closed)) => {
|
||||
let event_data: Vec<(&str, serde_json::Value)> = vec![
|
||||
("type", json!("closed")),
|
||||
("terminal_id", json!(closed.terminal_id)),
|
||||
("exit_code", json!(closed.exit_code)),
|
||||
];
|
||||
self.push_event_("terminal_response", &event_data, &[], &[]);
|
||||
}
|
||||
Some(Union::Error(error)) => {
|
||||
let event_data: Vec<(&str, serde_json::Value)> = vec![
|
||||
("type", json!("error")),
|
||||
("terminal_id", json!(error.terminal_id)),
|
||||
("message", json!(&error.message)),
|
||||
];
|
||||
self.push_event_("terminal_response", &event_data, &[], &[]);
|
||||
}
|
||||
None => {}
|
||||
Some(_) => {
|
||||
log::warn!("Unhandled terminal response type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FlutterHandler {
|
||||
@@ -1221,6 +1275,7 @@ pub fn session_add(
|
||||
is_view_camera: bool,
|
||||
is_port_forward: bool,
|
||||
is_rdp: bool,
|
||||
is_terminal: bool,
|
||||
switch_uuid: &str,
|
||||
force_relay: bool,
|
||||
password: String,
|
||||
@@ -1231,6 +1286,8 @@ pub fn session_add(
|
||||
ConnType::FILE_TRANSFER
|
||||
} else if is_view_camera {
|
||||
ConnType::VIEW_CAMERA
|
||||
} else if is_terminal {
|
||||
ConnType::TERMINAL
|
||||
} else if is_port_forward {
|
||||
if is_rdp {
|
||||
ConnType::RDP
|
||||
|
||||
@@ -103,6 +103,8 @@ pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn<usize>
|
||||
ConnType::PORT_FORWARD
|
||||
} else if conn_type == ConnType::RDP as i32 {
|
||||
ConnType::RDP
|
||||
} else if conn_type == ConnType::TERMINAL as i32 {
|
||||
ConnType::TERMINAL
|
||||
} else {
|
||||
ConnType::DEFAULT_CONN
|
||||
};
|
||||
@@ -129,6 +131,7 @@ pub fn session_add_sync(
|
||||
is_view_camera: bool,
|
||||
is_port_forward: bool,
|
||||
is_rdp: bool,
|
||||
is_terminal: bool,
|
||||
switch_uuid: String,
|
||||
force_relay: bool,
|
||||
password: String,
|
||||
@@ -142,6 +145,7 @@ pub fn session_add_sync(
|
||||
is_view_camera,
|
||||
is_port_forward,
|
||||
is_rdp,
|
||||
is_terminal,
|
||||
&switch_uuid,
|
||||
force_relay,
|
||||
password,
|
||||
@@ -613,6 +617,33 @@ pub fn session_send_chat(session_id: SessionID, text: String) {
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal functions
|
||||
pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.open_terminal(terminal_id, rows, cols);
|
||||
} else {
|
||||
log::error!("[flutter_ffi] Session not found for session_id: {}", session_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_send_terminal_input(session_id: SessionID, terminal_id: i32, data: String) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.send_terminal_input(terminal_id, data);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_resize_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.resize_terminal(terminal_id, rows, cols);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_close_terminal(session_id: SessionID, terminal_id: i32) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.close_terminal(terminal_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_peer_option(session_id: SessionID, name: String, value: String) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.set_option(name, value);
|
||||
|
||||
@@ -190,6 +190,7 @@ pub enum Data {
|
||||
id: i32,
|
||||
is_file_transfer: bool,
|
||||
is_view_camera: bool,
|
||||
is_terminal: bool,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
authorized: bool,
|
||||
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "لا يوجد اذن نقل الملف"),
|
||||
("Note", "ملاحظة"),
|
||||
("Connection", "الاتصال"),
|
||||
("Share Screen", "مشاركة الشاشة"),
|
||||
("Share screen", "مشاركة الشاشة"),
|
||||
("Chat", "محادثة"),
|
||||
("Total", "الاجمالي"),
|
||||
("items", "عناصر"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "لقط الشاشة"),
|
||||
("Input Control", "تحكم الادخال"),
|
||||
("Audio Capture", "لقط الصوت"),
|
||||
("File Connection", "اتصال الملف"),
|
||||
("Screen Connection", "اتصال الشاشة"),
|
||||
("Do you accept?", "هل تقبل؟"),
|
||||
("Open System Setting", "فتح اعدادات النظام"),
|
||||
("How to get Android input permission?", "كيف تحصل على اذن الادخال في اندرويد؟"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "غير موسوم"),
|
||||
("new-version-of-{}-tip", "تحديث جديد متاح لـ {}"),
|
||||
("Accessible devices", "الأجهزة القابلة للوصول"),
|
||||
("View camera", "عرض الكاميرا"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "ترقية عميل RustDesk البعيد إلى {}"),
|
||||
("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"),
|
||||
("Enable camera", "تمكين الكاميرا"),
|
||||
("No cameras", "لا توجد كاميرات"),
|
||||
("d3d_render_tip", "تمكين العرض باستخدام D3D"),
|
||||
("Use D3D rendering", "استخدام عرض D3D"),
|
||||
("Printer", "الطابعة"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "كلمة مرور رقمية لمرة واحدة"),
|
||||
("Enable IPv6 P2P connection", "تمكين اتصال نظير إلى نظير عبر IPv6"),
|
||||
("Enable UDP hole punching", "تمكين تقنية حفر الثغرات عبر UDP"),
|
||||
("View camera", "عرض الكاميرا"),
|
||||
("Enable camera", "تمكين الكاميرا"),
|
||||
("No cameras", "لا توجد كاميرات"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Няма дазволу на перадачу файлаў"),
|
||||
("Note", "Нататка"),
|
||||
("Connection", "Падключэнне"),
|
||||
("Share Screen", "Дзяліцца экранам"),
|
||||
("Share screen", "Дзяліцца экранам"),
|
||||
("Chat", "Чат"),
|
||||
("Total", "Усяго"),
|
||||
("items", "элементы"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Захоп экрана"),
|
||||
("Input Control", "Кіраванне ўводам"),
|
||||
("Audio Capture", "Захоп аўдыё"),
|
||||
("File Connection", "Падлучэнне перадачы файлаў"),
|
||||
("Screen Connection", "Падлучэнне прагляду/кіравання экранам"),
|
||||
("Do you accept?", "Ці вы згодны?"),
|
||||
("Open System Setting", "Адкрыць налады сістэмы"),
|
||||
("How to get Android input permission?", "Як атрымаць дазвол на ўвод Android?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Прагляд камеры"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Прагляд камеры"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"),
|
||||
("Note", "Бележка"),
|
||||
("Connection", "Връзка"),
|
||||
("Share Screen", "Сподели екран"),
|
||||
("Share screen", "Сподели екран"),
|
||||
("Chat", "Чат"),
|
||||
("Total", "Общо"),
|
||||
("items", "неща"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Заснемане на екрана"),
|
||||
("Input Control", "Управление на въвеждане"),
|
||||
("Audio Capture", "Аудиозапис"),
|
||||
("File Connection", "Файлова връзка"),
|
||||
("Screen Connection", "Екранна връзка"),
|
||||
("Do you accept?", "Приемате ли?"),
|
||||
("Open System Setting", "Отворете системните настройки"),
|
||||
("How to get Android input permission?", "Как да получим право за въвеждане при Андроид?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Преглед на камерата"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Преглед на камерата"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Cap permís per a transferència de fitxers"),
|
||||
("Note", "Nota"),
|
||||
("Connection", "Connexió"),
|
||||
("Share Screen", "Compartició de pantalla"),
|
||||
("Share screen", "Compartició de pantalla"),
|
||||
("Chat", "Xat"),
|
||||
("Total", "Total"),
|
||||
("items", "elements"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Captura de pantalla"),
|
||||
("Input Control", "Control d'entrada"),
|
||||
("Audio Capture", "Captura d'àudio"),
|
||||
("File Connection", "Connexió de fitxer"),
|
||||
("Screen Connection", "Connexió de pantalla"),
|
||||
("Do you accept?", "Voleu acceptar?"),
|
||||
("Open System Setting", "Obre la configuració del sistema"),
|
||||
("How to get Android input permission?", "Com modificar els permisos a Android?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sense etiquetar"),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", "Dispositius accessibles"),
|
||||
("View camera", "Mostra la càmera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Mostra la càmera"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "没有文件传输权限"),
|
||||
("Note", "备注"),
|
||||
("Connection", "连接"),
|
||||
("Share Screen", "共享屏幕"),
|
||||
("Share screen", "共享屏幕"),
|
||||
("Chat", "聊天消息"),
|
||||
("Total", "总计"),
|
||||
("items", "个项目"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "屏幕录制"),
|
||||
("Input Control", "输入控制"),
|
||||
("Audio Capture", "音频录制"),
|
||||
("File Connection", "文件连接"),
|
||||
("Screen Connection", "屏幕连接"),
|
||||
("Do you accept?", "是否接受?"),
|
||||
("Open System Setting", "打开系统设置"),
|
||||
("How to get Android input permission?", "如何获取安卓的输入权限?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "无标签"),
|
||||
("new-version-of-{}-tip", "{} 版本更新"),
|
||||
("Accessible devices", "可访问的设备"),
|
||||
("View camera", "查看摄像头"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"),
|
||||
("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"),
|
||||
("Enable camera", "允许查看摄像头"),
|
||||
("No cameras", "没有摄像头"),
|
||||
("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"),
|
||||
("Use D3D rendering", "使用 D3D 渲染"),
|
||||
("Printer", "打印机"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "一次性密码为数字"),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "查看摄像头"),
|
||||
("Enable camera", "允许查看摄像头"),
|
||||
("No cameras", "没有摄像头"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Žádné oprávnění k přenosu souborů"),
|
||||
("Note", "Poznámka"),
|
||||
("Connection", "Připojení"),
|
||||
("Share Screen", "Sdílet obrazovku"),
|
||||
("Share screen", "Sdílet obrazovku"),
|
||||
("Chat", "Chat"),
|
||||
("Total", "Celkem"),
|
||||
("items", "Položek"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Zachytávání obrazovky"),
|
||||
("Input Control", "Ovládání vstupních zařízení"),
|
||||
("Audio Capture", "Zachytávání zvuku"),
|
||||
("File Connection", "Souborové spojení"),
|
||||
("Screen Connection", "Spojení obrazovky"),
|
||||
("Do you accept?", "Přijímáte?"),
|
||||
("Open System Setting", "Otevřít nastavení systému"),
|
||||
("How to get Android input permission?", "Jak v systému Android získat oprávnění pro vstupní zařízení?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Zobrazit kameru"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Zobrazit kameru"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Ingen tilladelse til at overføre filen"),
|
||||
("Note", "Note"),
|
||||
("Connection", "Forbindelse"),
|
||||
("Share Screen", "Del skærmen"),
|
||||
("Share screen", "Del skærmen"),
|
||||
("Chat", "Chat"),
|
||||
("Total", "Total"),
|
||||
("items", "artikel"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Skærmoptagelse"),
|
||||
("Input Control", "Inputkontrol"),
|
||||
("Audio Capture", "Lydoptagelse"),
|
||||
("File Connection", "Filforbindelse"),
|
||||
("Screen Connection", "Færdiggørelse"),
|
||||
("Do you accept?", "Accepterer du?"),
|
||||
("Open System Setting", "Åbn systemindstillingen"),
|
||||
("How to get Android input permission?", "Hvordan får jeg en Android-input tilladelse?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Se kamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Se kamera"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"),
|
||||
("Note", "Hinweis"),
|
||||
("Connection", "Verbindung"),
|
||||
("Share Screen", "Bildschirm freigeben"),
|
||||
("Share screen", "Bildschirm freigeben"),
|
||||
("Chat", "Chat"),
|
||||
("Total", "Gesamt"),
|
||||
("items", "Einträge"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Bildschirmaufnahme"),
|
||||
("Input Control", "Eingabesteuerung"),
|
||||
("Audio Capture", "Audioaufnahme"),
|
||||
("File Connection", "Dateiverbindung"),
|
||||
("Screen Connection", "Bildschirmverbindung"),
|
||||
("Do you accept?", "Verbindung zulassen?"),
|
||||
("Open System Setting", "Systemeinstellung öffnen"),
|
||||
("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Unmarkiert"),
|
||||
("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"),
|
||||
("Accessible devices", "Erreichbare Geräte"),
|
||||
("View camera", "Kamera anzeigen"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"),
|
||||
("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."),
|
||||
("Enable camera", "Kamera zulassen"),
|
||||
("No cameras", "Keine Kameras"),
|
||||
("d3d_render_tip", "Wenn das D3D-Rendering aktiviert ist, kann der entfernte Bildschirm auf manchen Rechnern schwarz sein."),
|
||||
("Use D3D rendering", "D3D-Rendering verwenden"),
|
||||
("Printer", "Drucker"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "Numerisches Einmalpasswort"),
|
||||
("Enable IPv6 P2P connection", "IPv6-P2P-Verbindung aktivieren"),
|
||||
("Enable UDP hole punching", "UDP-Hole-Punching aktivieren"),
|
||||
("View camera", "Kamera anzeigen"),
|
||||
("Enable camera", "Kamera zulassen"),
|
||||
("No cameras", "Keine Kameras"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"),
|
||||
("Note", "Σημείωση"),
|
||||
("Connection", "Σύνδεση"),
|
||||
("Share Screen", "Κοινή χρήση οθόνης"),
|
||||
("Share screen", "Κοινή χρήση οθόνης"),
|
||||
("Chat", "Κουβέντα"),
|
||||
("Total", "Σύνολο"),
|
||||
("items", "στοιχεία"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Αποτύπωση οθόνης"),
|
||||
("Input Control", "Έλεγχος εισόδου"),
|
||||
("Audio Capture", "Εγγραφή ήχου"),
|
||||
("File Connection", "Σύνδεση αρχείου"),
|
||||
("Screen Connection", "Σύνδεση οθόνης"),
|
||||
("Do you accept?", "Δέχεσαι;"),
|
||||
("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"),
|
||||
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Χωρίς ετικέτα"),
|
||||
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
|
||||
("Accessible devices", "Προσβάσιμες συσκευές"),
|
||||
("View camera", "Προβολή κάμερας"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Προβολή κάμερας"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -77,12 +77,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Canvas Move", "Canvas move"),
|
||||
("Pinch to Zoom", "Pinch to zoom"),
|
||||
("Canvas Zoom", "Canvas zoom"),
|
||||
("Share Screen", "Share screen"),
|
||||
("Screen Capture", "Screen capture"),
|
||||
("Input Control", "Input control"),
|
||||
("Audio Capture", "Audio capture"),
|
||||
("File Connection", "File connection"),
|
||||
("Screen Connection", "Screen connection"),
|
||||
("Open System Setting", "Open system setting"),
|
||||
("android_input_permission_tip1", "In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the \"Accessibility\" service."),
|
||||
("android_input_permission_tip2", "Please go to the next system settings page, find and enter [Installed Services], turn on [RustDesk Input] service."),
|
||||
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Neniu permeso de dosiertransigo"),
|
||||
("Note", "Notu"),
|
||||
("Connection", "Konekto"),
|
||||
("Share Screen", "Kunhavigi Ekranon"),
|
||||
("Share screen", "Kunhavigi Ekranon"),
|
||||
("Chat", "Babilo"),
|
||||
("Total", "Sumo"),
|
||||
("items", "eroj"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Ekrankapto"),
|
||||
("Input Control", "Eniga Kontrolo"),
|
||||
("Audio Capture", "Sonkontrolo"),
|
||||
("File Connection", "Dosiero Konekto"),
|
||||
("Screen Connection", "Ekrono konekto"),
|
||||
("Do you accept?", "Ĉu vi akceptas?"),
|
||||
("Open System Setting", "Malfermi Sistemajn Agordojn"),
|
||||
("How to get Android input permission?", "Kiel akiri Android enigajn permesojn"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Rigardi kameron"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Rigardi kameron"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Sin permiso de transferencia de archivos"),
|
||||
("Note", "Nota"),
|
||||
("Connection", "Conexión"),
|
||||
("Share Screen", "Compartir pantalla"),
|
||||
("Share screen", "Compartir pantalla"),
|
||||
("Chat", "Chat"),
|
||||
("Total", "Total"),
|
||||
("items", "items"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Captura de pantalla"),
|
||||
("Input Control", "Control de entrada"),
|
||||
("Audio Capture", "Captura de audio"),
|
||||
("File Connection", "Conexión de archivos"),
|
||||
("Screen Connection", "Conexión de pantalla"),
|
||||
("Do you accept?", "¿Aceptas?"),
|
||||
("Open System Setting", "Configuración del sistema abierto"),
|
||||
("How to get Android input permission?", "¿Cómo obtener el permiso de entrada de Android?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sin itiquetar"),
|
||||
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Ver cámara"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"),
|
||||
("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."),
|
||||
("Enable camera", "Habilitar cámara"),
|
||||
("No cameras", "No hay cámaras"),
|
||||
("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."),
|
||||
("Use D3D rendering", "Usar renderizado D3D"),
|
||||
("Printer", "Impresora"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Ver cámara"),
|
||||
("Enable camera", "Habilitar cámara"),
|
||||
("No cameras", "No hay cámaras"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Failiülekande luba puudub"),
|
||||
("Note", "Märkus"),
|
||||
("Connection", "Ühendus"),
|
||||
("Share Screen", "Jaga ekraani"),
|
||||
("Share screen", "Jaga ekraani"),
|
||||
("Chat", "Vestlus"),
|
||||
("Total", "Kokku"),
|
||||
("items", "üksust"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Ekraanisalvestus"),
|
||||
("Input Control", "Sisendjuhtimine"),
|
||||
("Audio Capture", "Helisalvestus"),
|
||||
("File Connection", "Failiühendus"),
|
||||
("Screen Connection", "Kuvaühendus"),
|
||||
("Do you accept?", "Kas nõustud?"),
|
||||
("Open System Setting", "Ava süsteemisätted"),
|
||||
("How to get Android input permission?", "Kuidas saada Androidi sisendi luba?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sildistamata"),
|
||||
("new-version-of-{}-tip", "Saadaval on {} uus versioon"),
|
||||
("Accessible devices", "Ligipääsetavad seadmed"),
|
||||
("View camera", "Vaata kaamerat"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Vaata kaamerat"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Ez duzu baimenik fitxategiak transferitzeko"),
|
||||
("Note", "Nota"),
|
||||
("Connection", "Konexioa"),
|
||||
("Share Screen", "Partekatu pantaila"),
|
||||
("Share screen", "Partekatu pantaila"),
|
||||
("Chat", "Txata"),
|
||||
("Total", "Guztira"),
|
||||
("items", "elementuak"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Pantaila-grabazioa"),
|
||||
("Input Control", "Sarrera-kontrola"),
|
||||
("Audio Capture", "Audio-grabazioa"),
|
||||
("File Connection", "Fitxategi-konexioa"),
|
||||
("Screen Connection", "Pantaila-konexioa"),
|
||||
("Do you accept?", "Onartzen al duzu?"),
|
||||
("Open System Setting", "Ireki sistemaren ezarpenak"),
|
||||
("How to get Android input permission?", "Nola lortu dezaket Android sarrera-baimena?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Ikusi kamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Ikusi kamera"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "مجوز انتقال فایل داده نشده"),
|
||||
("Note", "یادداشت"),
|
||||
("Connection", "ارتباط"),
|
||||
("Share Screen", "اشتراک گذاری صفحه"),
|
||||
("Share screen", "اشتراک گذاری صفحه"),
|
||||
("Chat", "چت"),
|
||||
("Total", "مجموع"),
|
||||
("items", "آیتم ها"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "ضبط صفحه"),
|
||||
("Input Control", "کنترل ورودی"),
|
||||
("Audio Capture", "ضبط صدا"),
|
||||
("File Connection", "ارتباط فایل"),
|
||||
("Screen Connection", "ارتباط صفحه"),
|
||||
("Do you accept?", "آیا می پذیرید؟"),
|
||||
("Open System Setting", "باز کردن تنظیمات سیستم"),
|
||||
("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را دریافت کنیم؟"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "بدون برچسب"),
|
||||
("new-version-of-{}-tip", "نسخه جدید {} در دسترس است"),
|
||||
("Accessible devices", "دستگاههای در دسترس"),
|
||||
("View camera", "نمایش دوربین"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید"),
|
||||
("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمیشود"),
|
||||
("Enable camera", "فعال کردن دوربین"),
|
||||
("No cameras", "هیچ دوربینی یافت نشد"),
|
||||
("d3d_render_tip", "فعال کردن رندر D3D برای عملکرد بهتر"),
|
||||
("Use D3D rendering", "استفاده از رندر D3D"),
|
||||
("Printer", "چاپگر"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "رمز عبور یکبار مصرف عددی"),
|
||||
("Enable IPv6 P2P connection", "فعالسازی اتصال همتابههمتای IPv6"),
|
||||
("Enable UDP hole punching", "فعالسازی تکنیک UDP hole punching"),
|
||||
("View camera", "نمایش دوربین"),
|
||||
("Enable camera", "فعال کردن دوربین"),
|
||||
("No cameras", "هیچ دوربینی یافت نشد"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Absence de l’autorisation de transfert de fichiers"),
|
||||
("Note", "Note"),
|
||||
("Connection", "Connexion"),
|
||||
("Share Screen", "Partage d’écran"),
|
||||
("Share screen", "Partage d’écran"),
|
||||
("Chat", "Discussion"),
|
||||
("Total", "Total"),
|
||||
("items", "éléments"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Capture de l’écran"),
|
||||
("Input Control", "Contrôle de la saisie"),
|
||||
("Audio Capture", "Capture de l’audio"),
|
||||
("File Connection", "Connexion aux fichiers"),
|
||||
("Screen Connection", "Connexion à l’écran"),
|
||||
("Do you accept?", "Acceptez-vous ?"),
|
||||
("Open System Setting", "Ouvrir les paramètres système"),
|
||||
("How to get Android input permission?", "Comment obtenir l’autorisation de contrôle de la saisie sur Android ?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Sans étiquette"),
|
||||
("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"),
|
||||
("Accessible devices", "Appareils accessibles"),
|
||||
("View camera", "Afficher la caméra"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"),
|
||||
("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."),
|
||||
("Enable camera", "Activer la caméra"),
|
||||
("No cameras", "Aucune caméra"),
|
||||
("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."),
|
||||
("Use D3D rendering", "Utiliser le rendu D3D"),
|
||||
("Printer", "Imprimante"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "Mot de passe à usage unique numérique"),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Afficher la caméra"),
|
||||
("Enable camera", "Activer la caméra"),
|
||||
("No cameras", "Aucune caméra"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "ფაილების გადაცემის უფლება არ არის"),
|
||||
("Note", "შენიშვნა"),
|
||||
("Connection", "კავშირი"),
|
||||
("Share Screen", "ეკრანის დემონსტრაცია"),
|
||||
("Share screen", "ეკრანის დემონსტრაცია"),
|
||||
("Chat", "ჩატი"),
|
||||
("Total", "სულ"),
|
||||
("items", "ელემენტები"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "ეკრანის ჩაწერა"),
|
||||
("Input Control", "შეყვანის კონტროლი"),
|
||||
("Audio Capture", "აუდიოს ჩაწერა"),
|
||||
("File Connection", "ფაილების გადაცემის დაკავშირება"),
|
||||
("Screen Connection", "ეკრანის ნახვის/მართვის დაკავშირება"),
|
||||
("Do you accept?", "თანახმა ხართ?"),
|
||||
("Open System Setting", "სისტემის პარამეტრების გახსნა"),
|
||||
("How to get Android input permission?", "როგორ მივიღოთ Android-ის შეყვანის უფლება?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "უტეგო"),
|
||||
("new-version-of-{}-tip", "ხელმისაწვდომია ახალი ვერსია {}"),
|
||||
("Accessible devices", "ხელმისაწვდომი მოწყობილობები"),
|
||||
("View camera", "კამერის ნახვა"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "განაახლეთ RustDesk კლიენტი ვერსიამდე {} ან უფრო ახალი დისტანციურ მხარეზე!"),
|
||||
("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."),
|
||||
("Enable camera", "კამერის ჩართვა"),
|
||||
("No cameras", "კამერა არ არის"),
|
||||
("d3d_render_tip", "D3D ვიზუალიზაციის ჩართვისას ზოგიერთ მოწყობილობაზე დისტანციური ეკრანი შეიძლება იყოს შავი."),
|
||||
("Use D3D rendering", "D3D ვიზუალიზაციის გამოყენება"),
|
||||
("Printer", "პრინტერი"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "კამერის ნახვა"),
|
||||
("Enable camera", "კამერის ჩართვა"),
|
||||
("No cameras", "კამერა არ არის"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "אין הרשאת העברת קבצים"),
|
||||
("Note", "הערה"),
|
||||
("Connection", "התחברות"),
|
||||
("Share Screen", "שיתוף מסך"),
|
||||
("Share screen", "שיתוף מסך"),
|
||||
("Chat", "צ'אט"),
|
||||
("Total", "הכל"),
|
||||
("items", "פריטים"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "לכידת מסך"),
|
||||
("Input Control", "בקרת קלט"),
|
||||
("Audio Capture", "לכידת שמע"),
|
||||
("File Connection", "חיבור להעברת קבצים"),
|
||||
("Screen Connection", "חיבור תצוגה"),
|
||||
("Do you accept?", "האם אתה מקבל?"),
|
||||
("Open System Setting", "פתח הגדרות מערכת"),
|
||||
("How to get Android input permission?", "כיצד לקבל הרשאת קלט באנדרואיד?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "לא מתוייג"),
|
||||
("new-version-of-{}-tip", "גרסה חדשה של {} זמינה"),
|
||||
("Accessible devices", "מכשירים נגישים"),
|
||||
("View camera", "הצג מצלמה"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"),
|
||||
("view_camera_unsupported_tip", "הצגת מצלמה אינה נתמכת במכשיר המרוחק"),
|
||||
("Enable camera", "הפעל מצלמה"),
|
||||
("No cameras", "אין מצלמות"),
|
||||
("d3d_render_tip", "שימוש בעיבוד Direct3D עשוי לשפר ביצועים בחלק מהמקרים"),
|
||||
("Use D3D rendering", "השתמש בעיבוד D3D"),
|
||||
("Printer", "מדפסת"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "הצג מצלמה"),
|
||||
("Enable camera", "הפעל מצלמה"),
|
||||
("No cameras", "אין מצלמות"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Nemate pravo prijenosa datoteka"),
|
||||
("Note", "Bilješka"),
|
||||
("Connection", "Povezivanje"),
|
||||
("Share Screen", "Podijeli zaslon"),
|
||||
("Share screen", "Podijeli zaslon"),
|
||||
("Chat", "Dopisivanje"),
|
||||
("Total", "Ukupno"),
|
||||
("items", "stavki"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Snimanje zaslona"),
|
||||
("Input Control", "Kontrola unosa"),
|
||||
("Audio Capture", "Snimanje zvuka"),
|
||||
("File Connection", "Spajanje preko datoteke"),
|
||||
("Screen Connection", "Podijelite vezu"),
|
||||
("Do you accept?", "Prihvaćate li?"),
|
||||
("Open System Setting", "Postavke otvorenog sustava"),
|
||||
("How to get Android input permission?", "Kako dobiti pristup za unos na Androidu?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Pregled kamere"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Pregled kamere"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Nincs engedély a fájlátvitelre"),
|
||||
("Note", "Megjegyzés"),
|
||||
("Connection", "Kapcsolat"),
|
||||
("Share Screen", "Képernyőmegosztás"),
|
||||
("Share screen", "Képernyőmegosztás"),
|
||||
("Chat", "Csevegés"),
|
||||
("Total", "Összes"),
|
||||
("items", "elemek"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Képernyőrögzítés"),
|
||||
("Input Control", "Távoli vezérlés"),
|
||||
("Audio Capture", "Hangrögzítés"),
|
||||
("File Connection", "Fájlátvitel"),
|
||||
("Screen Connection", "Képátvitel"),
|
||||
("Do you accept?", "Elfogadás?"),
|
||||
("Open System Setting", "Rendszerbeállítások megnyitása"),
|
||||
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Címkézetlen"),
|
||||
("new-version-of-{}-tip", "A(z) {} új verziója"),
|
||||
("Accessible devices", "Hozzáférhető eszközök"),
|
||||
("View camera", "Kamera megtekintése"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"),
|
||||
("view_camera_unsupported_tip", "A kameranézet nem támogatott"),
|
||||
("Enable camera", "Kamera engedélyezése"),
|
||||
("No cameras", "Nincs kamera"),
|
||||
("d3d_render_tip", "D3D renderelés"),
|
||||
("Use D3D rendering", "D3D renderelés használata"),
|
||||
("Printer", "Nyomtató"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Kamera megtekintése"),
|
||||
("Enable camera", "Kamera engedélyezése"),
|
||||
("No cameras", "Nincs kamera"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Tidak ada izin untuk mengirim file"),
|
||||
("Note", "Catatan"),
|
||||
("Connection", "Koneksi"),
|
||||
("Share Screen", "Bagikan Layar"),
|
||||
("Share screen", "Bagikan Layar"),
|
||||
("Chat", "Obrolan"),
|
||||
("Total", "Total"),
|
||||
("items", "item"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Tangkapan Layar"),
|
||||
("Input Control", "Kontrol input"),
|
||||
("Audio Capture", "Rekam Suara"),
|
||||
("File Connection", "Koneksi File"),
|
||||
("Screen Connection", "Koneksi layar"),
|
||||
("Do you accept?", "Apakah anda setuju?"),
|
||||
("Open System Setting", "Buka Pengaturan Sistem"),
|
||||
("How to get Android input permission?", "Bagaimana cara mendapatkan izin input dari Android?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", "Versi {} sudah tersedia."),
|
||||
("Accessible devices", "Perangkat yang tersedia"),
|
||||
("View camera", "Lihat Kamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Silahkan perbarui aplikasi RustDesk ke versi {} atau yang lebih baru pada komputer yang akan terhubung!"),
|
||||
("view_camera_unsupported_tip", "Perangkat yang terhubung tidak mendukung tampilan kamera."),
|
||||
("Enable camera", "Aktifkan kamera"),
|
||||
("No cameras", "Tidak ada kamera"),
|
||||
("d3d_render_tip", "Ketika rendering D3D diaktifkan, layar kontrol jarak jauh bisa tampak hitam di beberapa komputer"),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Lihat Kamera"),
|
||||
("Enable camera", "Aktifkan kamera"),
|
||||
("No cameras", "Tidak ada kamera"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Nessun permesso per il trasferimento file"),
|
||||
("Note", "Nota"),
|
||||
("Connection", "Connessione"),
|
||||
("Share Screen", "Condividi schermo"),
|
||||
("Share screen", "Condividi schermo"),
|
||||
("Chat", "Chat"),
|
||||
("Total", "Totale"),
|
||||
("items", "Oggetti"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Cattura schermo"),
|
||||
("Input Control", "Controllo input"),
|
||||
("Audio Capture", "Acquisizione audio"),
|
||||
("File Connection", "Connessione file"),
|
||||
("Screen Connection", "Connessione schermo"),
|
||||
("Do you accept?", "Accetti?"),
|
||||
("Open System Setting", "Apri impostazioni di sistema"),
|
||||
("How to get Android input permission?", "Come ottenere l'autorizzazione input in Android?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Senza tag"),
|
||||
("new-version-of-{}-tip", "È disponibile una nuova versione di {}"),
|
||||
("Accessible devices", "Dispositivi accessibili"),
|
||||
("View camera", "Visualizza telecamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"),
|
||||
("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."),
|
||||
("Enable camera", "Abilita camera"),
|
||||
("No cameras", "Nessuna camera"),
|
||||
("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."),
|
||||
("Use D3D rendering", "Usa rendering D3D"),
|
||||
("Printer", "Stampante"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "Password numerica monouso"),
|
||||
("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"),
|
||||
("Enable UDP hole punching", "Abilita hole punching UDP"),
|
||||
("View camera", "Visualizza telecamera"),
|
||||
("Enable camera", "Abilita camera"),
|
||||
("No cameras", "Nessuna camera"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "ファイル転送の権限がありません"),
|
||||
("Note", "ノート"),
|
||||
("Connection", "接続"),
|
||||
("Share Screen", "画面を共有"),
|
||||
("Share screen", "画面を共有"),
|
||||
("Chat", "チャット"),
|
||||
("Total", "計"),
|
||||
("items", "個のアイテム"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "画面キャプチャ"),
|
||||
("Input Control", "入力操作"),
|
||||
("Audio Capture", "音声キャプチャ"),
|
||||
("File Connection", "ファイルの接続"),
|
||||
("Screen Connection", "画面の接続"),
|
||||
("Do you accept?", "許可しますか?"),
|
||||
("Open System Setting", "システム設定を開く"),
|
||||
("How to get Android input permission?", "Androidの入力権限を取得するには?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "カメラを表示"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "カメラを表示"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "파일 전송 권한이 없습니다."),
|
||||
("Note", "메모"),
|
||||
("Connection", "연결"),
|
||||
("Share Screen", "화면 공유"),
|
||||
("Share screen", "화면 공유"),
|
||||
("Chat", "채팅"),
|
||||
("Total", "총"),
|
||||
("items", "개"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "화면 캡처"),
|
||||
("Input Control", "입력 제어"),
|
||||
("Audio Capture", "오디오 캡처"),
|
||||
("File Connection", "파일 전송"),
|
||||
("Screen Connection", "화면 전송"),
|
||||
("Do you accept?", "수락하시겠습니까?"),
|
||||
("Open System Setting", "시스템 설정 열기"),
|
||||
("How to get Android input permission?", "Android 입력 권한을 얻는 방법"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "태그 없음"),
|
||||
("new-version-of-{}-tip", "{}의 새 버전이 출시되었습니다."),
|
||||
("Accessible devices", "연결 가능한 기기"),
|
||||
("View camera", "카메라 보기"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "원격 기기의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"),
|
||||
("view_camera_unsupported_tip", "원격 기기에서 카메라 보기를 지원하지 않습니다."),
|
||||
("Enable camera", "카메라 보기 허용"),
|
||||
("No cameras", "카메라 없음"),
|
||||
("d3d_render_tip", "D3D 렌더링을 활성화하면 일부 기기에서 원격 화면이 표시되지 않을 수 있습니다."),
|
||||
("Use D3D rendering", "D3D 렌더링 활성화"),
|
||||
("Printer", "프린터"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "일회용 비밀번호"),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "카메라 보기"),
|
||||
("Enable camera", "카메라 보기 허용"),
|
||||
("No cameras", "카메라 없음"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"),
|
||||
("Note", "Нота"),
|
||||
("Connection", "Қосылым"),
|
||||
("Share Screen", "Екіренді Бөлісу"),
|
||||
("Share screen", "Екіренді Бөлісу"),
|
||||
("Chat", "Чат"),
|
||||
("Total", "Барлығы"),
|
||||
("items", "зат"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Екіренді Түсіру"),
|
||||
("Input Control", "Еңгізуді Басқару/Қадағалау"),
|
||||
("Audio Capture", "Аудио Түсіру"),
|
||||
("File Connection", "Файыл Қосылымы"),
|
||||
("Screen Connection", "Екірен Қосылымы"),
|
||||
("Do you accept?", "Қабылдайсыз ба?"),
|
||||
("Open System Setting", "Жүйе Орнатпаларын Ашу"),
|
||||
("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Камераны Көру"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Камераны Көру"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Nėra leidimo perkelti failus"),
|
||||
("Note", "Pastaba"),
|
||||
("Connection", "Ryšys"),
|
||||
("Share Screen", "Bendrinti ekraną"),
|
||||
("Share screen", "Bendrinti ekraną"),
|
||||
("Chat", "Pokalbis"),
|
||||
("Total", "Iš viso"),
|
||||
("items", "elementai"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("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ą?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Peržiūrėti kamerą"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Peržiūrėti kamerą"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Nav atļaujas failu pārsūtīšanai"),
|
||||
("Note", "Piezīme"),
|
||||
("Connection", "Savienojums"),
|
||||
("Share Screen", "Koplietot ekrānu"),
|
||||
("Share screen", "Koplietot ekrānu"),
|
||||
("Chat", "Tērzēšana"),
|
||||
("Total", "Kopā"),
|
||||
("items", "vienumi"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Ekrāna tveršana"),
|
||||
("Input Control", "Ievades vadība"),
|
||||
("Audio Capture", "Audio tveršana"),
|
||||
("File Connection", "Failu savienojums"),
|
||||
("Screen Connection", "Ekrāna savienojums"),
|
||||
("Do you accept?", "Vai Jūs pieņemat?"),
|
||||
("Open System Setting", "Atvērt sistēmas iestatījumus"),
|
||||
("How to get Android input permission?", "Kā iegūt Android ievades atļauju?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", "Neatzīmēts"),
|
||||
("new-version-of-{}-tip", "Ir pieejama jauna {} versija"),
|
||||
("Accessible devices", "Pieejamas ierīces"),
|
||||
("View camera", "Skatīt kameru"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"),
|
||||
("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."),
|
||||
("Enable camera", "Iespējot kameru"),
|
||||
("No cameras", "Nav kameru"),
|
||||
("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."),
|
||||
("Use D3D rendering", "Izmantot D3D renderēšanu"),
|
||||
("Printer", "Printeris"),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", "Vienreiz lietojama ciparu parole"),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Skatīt kameru"),
|
||||
("Enable camera", "Iespējot kameru"),
|
||||
("No cameras", "Nav kameru"),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+8
-6
@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No permission of file transfer", "Ingen tillatelse til å overføre filen"),
|
||||
("Note", "Notat"),
|
||||
("Connection", "Tilkobling"),
|
||||
("Share Screen", "Del skjermen"),
|
||||
("Share screen", "Del skjermen"),
|
||||
("Chat", "Chat"),
|
||||
("Total", "Total"),
|
||||
("items", "Objekter"),
|
||||
@@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Screen Capture", "Skjermopptak"),
|
||||
("Input Control", "Input kontroll"),
|
||||
("Audio Capture", "Lydopptak"),
|
||||
("File Connection", "Filtilkobling"),
|
||||
("Screen Connection", "Skjermtilkobing"),
|
||||
("Do you accept?", "Akepterer du?"),
|
||||
("Open System Setting", "Åpne systeminnstillinger"),
|
||||
("How to get Android input permission?", "Hvordan får jeg en Android-input tillatelse?"),
|
||||
@@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Accessible devices", ""),
|
||||
("View camera", "Vis kamera"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
|
||||
("view_camera_unsupported_tip", ""),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("d3d_render_tip", ""),
|
||||
("Use D3D rendering", ""),
|
||||
("Printer", ""),
|
||||
@@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("View camera", "Vis kamera"),
|
||||
("Enable camera", ""),
|
||||
("No cameras", ""),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user