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:
RustDesk
2025-07-01 13:12:55 +08:00
committed by GitHub
parent ee5cdc3155
commit 5faf0ad3cf
130 changed files with 4064 additions and 4247 deletions

View File

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

View File

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

View File

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

142
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')),

View File

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

View File

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

View File

@@ -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'],

View File

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

View File

@@ -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: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,7 @@ enum DesktopTabType {
fileTransfer,
viewCamera,
portForward,
terminal,
install,
}

View File

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

View File

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

View File

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

View File

@@ -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: []);

View File

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

View File

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

View File

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

View File

@@ -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: []);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1 +0,0 @@
v1 is not compatible with current Flutter source code.

View File

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

View File

@@ -1 +0,0 @@
* text=auto

View File

@@ -1,9 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
*log
ogvjs
.vscode
.yarn

View File

@@ -1 +0,0 @@
nodeLinker: node-modules

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
import "./globals";
import "./ui";

View File

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

View File

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

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

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

View File

@@ -1 +0,0 @@
Under dev.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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."),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("No permission of file transfer", "Absence de lautorisation 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 laudio"),
("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 lautorisation 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", "Lappareil distant ne prend pas en charge laffichage 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 lutilisation 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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More