From 5faf0ad3cfef1b7237e6cbca53038d76ffdc281d Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:12:55 +0800 Subject: [PATCH] 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... --- .github/workflows/bridge.yml | 4 +- .github/workflows/flutter-build.yml | 16 +- .github/workflows/playground.yml | 4 +- Cargo.lock | 142 +- Cargo.toml | 2 +- .../com/carriez/flutter_hbb/MainService.kt | 4 +- flutter/lib/common.dart | 97 +- flutter/lib/common/widgets/peer_card.dart | 20 +- .../lib/common/widgets/setting_widgets.dart | 3 +- flutter/lib/common/widgets/toolbar.dart | 18 +- flutter/lib/consts.dart | 5 +- .../lib/desktop/pages/connection_page.dart | 51 +- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 2 + flutter/lib/desktop/pages/server_page.dart | 32 +- .../pages/terminal_connection_manager.dart | 98 ++ flutter/lib/desktop/pages/terminal_page.dart | 121 ++ .../lib/desktop/pages/terminal_tab_page.dart | 384 +++++ .../lib/desktop/pages/view_camera_page.dart | 2 - .../screen/desktop_terminal_screen.dart | 27 + .../lib/desktop/widgets/tabbar_widget.dart | 1 + flutter/lib/main.dart | 15 + .../lib/mobile/pages/file_manager_page.dart | 6 +- flutter/lib/mobile/pages/home_page.dart | 13 +- flutter/lib/mobile/pages/remote_page.dart | 4 +- flutter/lib/mobile/pages/server_page.dart | 6 +- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/mobile/pages/terminal_page.dart | 106 ++ .../lib/mobile/pages/view_camera_page.dart | 4 +- flutter/lib/models/model.dart | 63 +- flutter/lib/models/server_model.dart | 16 +- flutter/lib/models/terminal_model.dart | 269 +++ flutter/lib/utils/multi_window_manager.dart | 34 + flutter/lib/web/bridge.dart | 62 +- flutter/macos/Podfile.lock | 52 +- flutter/pubspec.lock | 459 ++--- flutter/pubspec.yaml | 6 +- flutter/web/v1/.gitignore | 9 - flutter/web/v1/README.md | 1 - flutter/web/v1/index.html | 183 -- flutter/web/v1/js/.gitattributes | 1 - flutter/web/v1/js/.gitignore | 9 - flutter/web/v1/js/.yarnrc.yml | 1 - flutter/web/v1/js/gen_js_from_hbb.py | 77 - flutter/web/v1/js/index.html | 15 - flutter/web/v1/js/package.json | 22 - flutter/web/v1/js/src/codec.js | 43 - flutter/web/v1/js/src/common.ts | 77 - flutter/web/v1/js/src/connection.ts | 773 --------- flutter/web/v1/js/src/globals.js | 383 ----- flutter/web/v1/js/src/main.ts | 2 - flutter/web/v1/js/src/style.css | 8 - flutter/web/v1/js/src/ui.js | 108 -- flutter/web/v1/js/src/vite-env.d.ts | 1 - flutter/web/v1/js/src/websock.ts | 183 -- flutter/web/v1/js/ts_proto.py | 20 - flutter/web/v1/js/tsconfig.json | 24 - flutter/web/v1/js/vite.config.js | 14 - flutter/web/v1/js/yarn.lock | 1494 ----------------- flutter/web/v1/libs/firebase-analytics.js | 2 - flutter/web/v1/libs/firebase-app.js | 2 - flutter/web/v1/manifest.json | 35 - flutter/web/v1/yarn.lock | 4 - flutter/web/v1/yuv.js | 73 - flutter/web/v1/yuv.wasm | Bin 8238 -> 0 bytes flutter/web/v2/README.md | 1 - libs/hbb_common | 2 +- src/client.rs | 48 +- src/client/io_loop.rs | 73 +- src/flutter.rs | 57 + src/flutter_ffi.rs | 31 + src/ipc.rs | 1 + src/lang/ar.rs | 14 +- src/lang/be.rs | 14 +- src/lang/bg.rs | 14 +- src/lang/ca.rs | 14 +- src/lang/cn.rs | 14 +- src/lang/cs.rs | 14 +- src/lang/da.rs | 14 +- src/lang/de.rs | 14 +- src/lang/el.rs | 14 +- src/lang/en.rs | 3 - src/lang/eo.rs | 14 +- src/lang/es.rs | 14 +- src/lang/et.rs | 14 +- src/lang/eu.rs | 14 +- src/lang/fa.rs | 14 +- src/lang/fr.rs | 14 +- src/lang/ge.rs | 14 +- src/lang/he.rs | 14 +- src/lang/hr.rs | 14 +- src/lang/hu.rs | 14 +- src/lang/id.rs | 14 +- src/lang/it.rs | 14 +- src/lang/ja.rs | 14 +- src/lang/ko.rs | 14 +- src/lang/kz.rs | 14 +- src/lang/lt.rs | 14 +- src/lang/lv.rs | 14 +- src/lang/nb.rs | 14 +- src/lang/nl.rs | 14 +- src/lang/pl.rs | 14 +- src/lang/pt_PT.rs | 14 +- src/lang/ptbr.rs | 14 +- src/lang/ro.rs | 14 +- src/lang/ru.rs | 14 +- src/lang/sc.rs | 14 +- src/lang/sk.rs | 14 +- src/lang/sl.rs | 14 +- src/lang/sq.rs | 14 +- src/lang/sr.rs | 14 +- src/lang/sv.rs | 14 +- src/lang/ta.rs | 14 +- src/lang/template.rs | 14 +- src/lang/th.rs | 14 +- src/lang/tr.rs | 14 +- src/lang/tw.rs | 14 +- src/lang/uk.rs | 14 +- src/lang/vi.rs | 14 +- src/lib.rs | 2 +- src/server.rs | 3 + src/server/connection.rs | 126 +- src/server/terminal_service.rs | 968 +++++++++++ src/ui/cm.rs | 1 + src/ui/cm.tis | 17 +- src/ui/index.tis | 4 +- src/ui/remote.rs | 13 +- src/ui_cm_interface.rs | 11 +- src/ui_session_interface.rs | 70 +- terminal.md | 521 ++++++ 130 files changed, 4064 insertions(+), 4247 deletions(-) create mode 100644 flutter/lib/desktop/pages/terminal_connection_manager.dart create mode 100644 flutter/lib/desktop/pages/terminal_page.dart create mode 100644 flutter/lib/desktop/pages/terminal_tab_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_terminal_screen.dart create mode 100644 flutter/lib/mobile/pages/terminal_page.dart create mode 100644 flutter/lib/models/terminal_model.dart delete mode 100644 flutter/web/v1/.gitignore delete mode 100644 flutter/web/v1/README.md delete mode 100644 flutter/web/v1/index.html delete mode 100644 flutter/web/v1/js/.gitattributes delete mode 100644 flutter/web/v1/js/.gitignore delete mode 100644 flutter/web/v1/js/.yarnrc.yml delete mode 100755 flutter/web/v1/js/gen_js_from_hbb.py delete mode 100644 flutter/web/v1/js/index.html delete mode 100644 flutter/web/v1/js/package.json delete mode 100644 flutter/web/v1/js/src/codec.js delete mode 100644 flutter/web/v1/js/src/common.ts delete mode 100644 flutter/web/v1/js/src/connection.ts delete mode 100644 flutter/web/v1/js/src/globals.js delete mode 100644 flutter/web/v1/js/src/main.ts delete mode 100644 flutter/web/v1/js/src/style.css delete mode 100644 flutter/web/v1/js/src/ui.js delete mode 100644 flutter/web/v1/js/src/vite-env.d.ts delete mode 100644 flutter/web/v1/js/src/websock.ts delete mode 100755 flutter/web/v1/js/ts_proto.py delete mode 100644 flutter/web/v1/js/tsconfig.json delete mode 100644 flutter/web/v1/js/vite.config.js delete mode 100644 flutter/web/v1/js/yarn.lock delete mode 100644 flutter/web/v1/libs/firebase-analytics.js delete mode 100644 flutter/web/v1/libs/firebase-app.js delete mode 100644 flutter/web/v1/manifest.json delete mode 100644 flutter/web/v1/yarn.lock delete mode 100644 flutter/web/v1/yuv.js delete mode 100644 flutter/web/v1/yuv.wasm delete mode 100644 flutter/web/v2/README.md create mode 100644 src/server/terminal_service.rs create mode 100644 terminal.md diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index d91dc25fb..1913132e2 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -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 \ diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 27b6118b9..36bbe7902 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -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 \ diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 962df73f1..48e5c4df0 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -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 \ diff --git a/Cargo.lock b/Cargo.lock index 440e02422..20e1b34ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 7108e923b..06bfcaeb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index c475dd3b0..7bb16a00a 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -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) { diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 5b58639ba..0fc8aa6c0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -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? 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? 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? 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? 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? 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 args = List.empty(growable: true); @@ -2342,6 +2360,7 @@ List? 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), ), ); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8c4b019b2..d664f3d80 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -491,6 +491,7 @@ abstract class BasePeerCard extends StatelessWidget { bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, + bool isTerminal = false, }) { return MenuEntryButton( 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 _terminalAction(BuildContext context) { + return _connectCommonAction( + context, + translate('Terminal'), + isTerminal: true, + ); + } + @protected MenuEntryBase _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); diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index cf8e89737..b57657274 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -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; diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index c861a0977..ee05e52a3 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -154,36 +154,38 @@ List 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')), diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a3063d9c7..d608df83a 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -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"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 608960378..41553b8db 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -327,10 +327,15 @@ class _ConnectionPageState extends State /// 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 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 'View camera', () => onConnect(isViewCamera: true) ), + ( + 'Terminal', + () => onConnect(isTerminal: true) + ), ] .map((e) => MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -583,8 +593,9 @@ class _ConnectionPageState extends State _menuOpen.value = false; }); }, - ); - }), + )); + }, + ), ), ), ]), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 07421d14f..0f302d8e1 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -786,6 +786,7 @@ class _DesktopHomePageState extends State 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'], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 455ab6c11..9d182f9f8 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -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), diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index ec081d574..4ee29756f 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -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: [ diff --git a/flutter/lib/desktop/pages/terminal_connection_manager.dart b/flutter/lib/desktop/pages/terminal_connection_manager.dart new file mode 100644 index 000000000..91b8baa97 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_connection_manager.dart @@ -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 _connections = {}; + static final Map _connectionRefCount = {}; + + // Track service IDs per peer + static final Map _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, 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(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'); + } +} \ No newline at end of file diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart new file mode 100644 index 000000000..f28545415 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -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 createState() => _TerminalPageState(); +} + +class _TerminalPageState extends State + 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; +} diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart new file mode 100644 index 000000000..ee2529107 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -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 params; + + const TerminalTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _TerminalTabPageState(params); +} + +class _TerminalTabPageState extends State { + DesktopTabController get tabController => Get.find(); + + static const IconData selectedIcon = Icons.terminal; + static const IconData unselectedIcon = Icons.terminal_outlined; + int _nextTerminalId = 1; + + _TerminalTabPageState(Map 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> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + + // New tab menu item + menu.add(MenuEntryButton( + 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( + switchType: SwitchType.scheckbox, + text: translate('Keep terminal sessions on disconnect'), + getter: () async { + final ffi = Get.find(tag: 'terminal_$peerId'); + return bind.sessionGetToggleOptionSync( + sessionId: ffi.sessionId, + arg: kOptionTerminalPersistent, + ); + }, + setter: (bool v) async { + final ffi = Get.find(tag: 'terminal_$peerId'); + bind.sessionToggleOption( + sessionId: ffi.sessionId, + value: kOptionTerminalPersistent, + ); + }, + padding: padding, + )); + + return mod_menu.PopupMenu( + 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 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; + } + } +} diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index d86114014..a1cc5c8a0 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -515,8 +515,6 @@ class ImagePaint extends StatefulWidget { } class _ImagePaintState extends State { - bool _lastRemoteCursorMoved = false; - String get id => widget.id; RxBool get cursorOverImage => widget.cursorOverImage; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; diff --git a/flutter/lib/desktop/screen/desktop_terminal_screen.dart b/flutter/lib/desktop/screen/desktop_terminal_screen.dart new file mode 100644 index 000000000..301489c86 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_terminal_screen.dart @@ -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 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, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 000690173..c1cc433ad 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -54,6 +54,7 @@ enum DesktopTabType { fileTransfer, viewCamera, portForward, + terminal, install, } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index f04e142d9..80a3bff89 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -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 main(List 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); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index b837dc276..3faf8f8b0 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -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 createState() => _FileManagerPageState(); @@ -74,7 +75,8 @@ class _FileManagerPageState extends State { 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); diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 82d7058ab..e35c8872c 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -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); } } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 93f49f585..b707fd38f 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -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 createState() => _RemotePageState(id); @@ -89,6 +90,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { widget.id, password: widget.password, isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index e7f022697..ed4fe4d98 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -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), diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 693bdbd30..505b0ff04 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -815,7 +815,7 @@ class _SettingsState extends State with WidgetsBindingObserver { !outgoingOnly && !hideSecuritySettings) SettingsSection( - title: Text(translate("Share Screen")), + title: Text(translate("Share screen")), tiles: shareScreenTiles, ), if (!bind.isIncomingOnly()) defaultDisplaySection(), diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart new file mode 100644 index 000000000..d7d17994c --- /dev/null +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -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 createState() => _TerminalPageState(); +} + +class _TerminalPageState extends State + 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; +} diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index ac70a2dab..1b668673a 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -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 createState() => _ViewCameraPageState(id); @@ -88,6 +89,7 @@ class _ViewCameraPageState extends State isViewCamera: true, password: widget.password, isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c28d3c1d6..b15112025 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -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 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 _terminalModels = {}; + + // Getter for terminal models + Map 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 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 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 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; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9c46e0797..c3e6fab71 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -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 { diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart new file mode 100644 index 000000000..3284c539b --- /dev/null +++ b/flutter/lib/models/terminal_model.dart @@ -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 = []; + + Future _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 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 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 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 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 _processBufferedInputAsync() async { + final buffer = List.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 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.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 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 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(); + } +} diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 4e848ea7c..a7b06b5c7 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -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 _fileTransferWindows = List.empty(growable: true); final List _viewCameraWindows = List.empty(growable: true); final List _portForwardWindows = List.empty(growable: true); + final List _terminalWindows = List.empty(growable: true); moveTabToNewWindow(int windowId, String peerId, String sessionId, WindowType windowType) async { @@ -343,6 +347,32 @@ class RustDeskMultiWindowManager { ); } + Future 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 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; } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 0ec3e076f..f1839c630 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -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 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 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 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 sessionCloseTerminal( + {required UuidValue sessionId, + required int terminalId, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'close_terminal', + jsonEncode({ + 'terminal_id': terminalId, + }) + ])); + } + void dispose() {} } diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a9f3c7388..008344842 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -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 diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 24a1897ad..aba6c7879 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,10 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" after_layout: dependency: transitive description: @@ -21,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.7.0" animations: dependency: transitive description: @@ -45,18 +50,18 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" auto_size_text: dependency: "direct main" description: @@ -69,10 +74,10 @@ packages: dependency: "direct main" description: name: auto_size_text_field - sha256: d47c81ffa9b61d219f6c50492dc03ea28fa9346561b2ec33b46ccdc000ddb0aa + sha256: "41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" back_button_interceptor: dependency: "direct main" description: @@ -85,10 +90,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" bot_toast: dependency: "direct main" description: @@ -125,10 +130,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -141,18 +146,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -165,10 +170,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.10.1" cached_network_image: dependency: transitive description: @@ -189,10 +194,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" characters: dependency: transitive description: @@ -205,10 +210,10 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -221,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -237,10 +242,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: @@ -261,42 +266,42 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.7" dash_chat_2: dependency: "direct main" description: @@ -310,10 +315,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" debounce_throttle: dependency: "direct main" description: @@ -335,7 +340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" + resolved-ref: b47e8385e5a75d38319ad706a64b0ead3108b093 url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -351,10 +356,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.2" draggable_float_widget: dependency: "direct main" description: @@ -380,6 +385,14 @@ packages: url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" extended_text: dependency: "direct main" description: @@ -392,10 +405,10 @@ packages: dependency: transitive description: name: extended_text_library - sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" external_path: dependency: "direct main" description: @@ -440,18 +453,18 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -464,34 +477,34 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+4" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" + sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.6.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "3.4.1" flutter: dependency: "direct main" description: flutter @@ -608,7 +621,7 @@ packages: source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: - dependency: transitive + dependency: "direct overridden" description: name: flutter_plugin_android_lifecycle sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da @@ -627,10 +640,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -640,74 +653,74 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" get: dependency: "direct main" description: name: get - sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.6" + version: "4.7.2" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.6" http: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -728,10 +741,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.3.0" image_picker: dependency: "direct main" description: @@ -744,50 +757,50 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.12+21" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.6" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.12+2" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.10.1" image_picker_windows: dependency: transitive description: @@ -808,10 +821,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -824,10 +837,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" lints: dependency: transitive description: @@ -840,18 +853,26 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -872,10 +893,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" nested: dependency: transitive description: @@ -888,18 +909,18 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -936,34 +957,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -984,10 +1005,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" pedantic: dependency: transitive description: @@ -1000,10 +1021,10 @@ packages: dependency: "direct main" description: name: percent_indicator - sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + sha256: "157d29133bbc6ecb11f923d36e7960a96a3f28837549a20b65e5135729f0f9fd" url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.5" petitparser: dependency: transitive description: @@ -1016,10 +1037,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1040,50 +1061,50 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.4.0" pull_down_button: dependency: "direct main" description: name: pull_down_button - sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" + sha256: "48b928203afdeafa4a8be5dc96980523bc8a2ddbd04569f766071a722be22379" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4" puppeteer: dependency: transitive description: name: puppeteer - sha256: eedeaae6ec5d2e54f9ae22ab4d6b3dda2e8791c356cc783046d06c287ffe11d8 + sha256: "7a990c68d33882b642214c351f66492d9a738afa4226a098ab70642357337fa2" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.16.0" qr: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" qr_code_scanner: dependency: "direct main" description: @@ -1104,10 +1125,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" rxdart: dependency: transitive description: @@ -1152,10 +1173,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1189,82 +1210,82 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sqflite: - dependency: transitive + dependency: "direct main" description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: a9a8c6dfdf315f87f2a23a7bad2b60c8d5af0f88a5fde92cf9205202770c2753 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4+6" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.6" texture_rgba_renderer: dependency: "direct main" description: @@ -1278,18 +1299,18 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" toggle_switch: dependency: "direct main" description: name: toggle_switch - sha256: "9e6af1f0c5a97d9de41109dc7b9e1b3bbe73417f89b10e0e44dc834fb493d4cb" + sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" tuple: dependency: "direct main" description: @@ -1302,10 +1323,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" uni_links: dependency: "direct main" description: @@ -1367,50 +1388,50 @@ packages: dependency: "direct main" description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1423,26 +1444,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1455,42 +1476,42 @@ packages: dependency: transitive description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.9.5" video_player_android: dependency: transitive description: name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + sha256: "391e092ba4abe2f93b3e625bd6b6a6ec7d7414279462c1c0ee42b5ab8d0a0898" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.7.16" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.7.1" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.5" visibility_detector: dependency: "direct main" description: @@ -1503,50 +1524,50 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.3" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" web: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" win32: dependency: "direct main" description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.10.1" win32_registry: dependency: transitive description: @@ -1577,10 +1598,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1589,30 +1610,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xterm: + dependency: "direct main" + description: + name: xterm + sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2" + url: "https://pub.dev" + source: hosted + version: "4.0.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yaml_edit: dependency: transitive description: name: yaml_edit - sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.2" + zmodem: + dependency: transitive + description: + name: zmodem + sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" + url: "https://pub.dev" + source: hosted + version: "0.0.6" zxing2: dependency: "direct main" description: name: zxing2 - sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 + sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.4" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ac9b753fc..03de1a4eb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -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 - diff --git a/flutter/web/v1/.gitignore b/flutter/web/v1/.gitignore deleted file mode 100644 index 6290a3f63..000000000 --- a/flutter/web/v1/.gitignore +++ /dev/null @@ -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 diff --git a/flutter/web/v1/README.md b/flutter/web/v1/README.md deleted file mode 100644 index b9e2fc5c0..000000000 --- a/flutter/web/v1/README.md +++ /dev/null @@ -1 +0,0 @@ -v1 is not compatible with current Flutter source code. \ No newline at end of file diff --git a/flutter/web/v1/index.html b/flutter/web/v1/index.html deleted file mode 100644 index b466df662..000000000 --- a/flutter/web/v1/index.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - RustDesk - - - - - - - - - - -
-
-
- - - - - - - - - diff --git a/flutter/web/v1/js/.gitattributes b/flutter/web/v1/js/.gitattributes deleted file mode 100644 index 176a458f9..000000000 --- a/flutter/web/v1/js/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto diff --git a/flutter/web/v1/js/.gitignore b/flutter/web/v1/js/.gitignore deleted file mode 100644 index 8737dbba5..000000000 --- a/flutter/web/v1/js/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules -.DS_Store -dist -dist-ssr -*.local -*log -ogvjs -.vscode -.yarn diff --git a/flutter/web/v1/js/.yarnrc.yml b/flutter/web/v1/js/.yarnrc.yml deleted file mode 100644 index 3186f3f07..000000000 --- a/flutter/web/v1/js/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/flutter/web/v1/js/gen_js_from_hbb.py b/flutter/web/v1/js/gen_js_from_hbb.py deleted file mode 100755 index cfa95ffe0..000000000 --- a/flutter/web/v1/js/gen_js_from_hbb.py +++ /dev/null @@ -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() diff --git a/flutter/web/v1/js/index.html b/flutter/web/v1/js/index.html deleted file mode 100644 index 0ae0a2410..000000000 --- a/flutter/web/v1/js/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - Vite App - - -
- - - diff --git a/flutter/web/v1/js/package.json b/flutter/web/v1/js/package.json deleted file mode 100644 index 15e0e75b8..000000000 --- a/flutter/web/v1/js/package.json +++ /dev/null @@ -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" - } -} diff --git a/flutter/web/v1/js/src/codec.js b/flutter/web/v1/js/src/codec.js deleted file mode 100644 index 27c9565ec..000000000 --- a/flutter/web/v1/js/src/codec.js +++ /dev/null @@ -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 } - ); -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/common.ts b/flutter/web/v1/js/src/common.ts deleted file mode 100644 index 8da049a4d..000000000 --- a/flutter/web/v1/js/src/common.ts +++ /dev/null @@ -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 ""; - } -} diff --git a/flutter/web/v1/js/src/connection.ts b/flutter/web/v1/js/src/connection.ts deleted file mode 100644 index b0c479c90..000000000 --- a/flutter/web/v1/js/src/connection.ts +++ /dev/null @@ -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(); -} diff --git a/flutter/web/v1/js/src/globals.js b/flutter/web/v1/js/src/globals.js deleted file mode 100644 index 953add18d..000000000 --- a/flutter/web/v1/js/src/globals.js +++ /dev/null @@ -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); - } - } -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/main.ts b/flutter/web/v1/js/src/main.ts deleted file mode 100644 index 2be877f58..000000000 --- a/flutter/web/v1/js/src/main.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./globals"; -import "./ui"; \ No newline at end of file diff --git a/flutter/web/v1/js/src/style.css b/flutter/web/v1/js/src/style.css deleted file mode 100644 index 852de7aa2..000000000 --- a/flutter/web/v1/js/src/style.css +++ /dev/null @@ -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; -} diff --git a/flutter/web/v1/js/src/ui.js b/flutter/web/v1/js/src/ui.js deleted file mode 100644 index 446334022..000000000 --- a/flutter/web/v1/js/src/ui.js +++ /dev/null @@ -1,108 +0,0 @@ -import "./style.css"; -import "./connection"; -import * as globals from "./globals"; - -const app = document.querySelector('#app'); - -if (app) { - app.innerHTML = ` -
- - - - -
Host:
Key:
Id:
- - - -`; - - 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 = '
' + text + '
'; - } else { - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#text').innerHTML = '
' + text + '
'; - } - } - - 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); - } - } -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/vite-env.d.ts b/flutter/web/v1/js/src/vite-env.d.ts deleted file mode 100644 index 151aa6856..000000000 --- a/flutter/web/v1/js/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/flutter/web/v1/js/src/websock.ts b/flutter/web/v1/js/src/websock.ts deleted file mode 100644 index 6f05e6f6b..000000000 --- a/flutter/web/v1/js/src/websock.ts +++ /dev/null @@ -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) { - 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) { - 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 { - 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 { - 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); - } -} diff --git a/flutter/web/v1/js/ts_proto.py b/flutter/web/v1/js/ts_proto.py deleted file mode 100755 index 62a73fe7c..000000000 --- a/flutter/web/v1/js/ts_proto.py +++ /dev/null @@ -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) diff --git a/flutter/web/v1/js/tsconfig.json b/flutter/web/v1/js/tsconfig.json deleted file mode 100644 index ca949de6a..000000000 --- a/flutter/web/v1/js/tsconfig.json +++ /dev/null @@ -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" - ] -} \ No newline at end of file diff --git a/flutter/web/v1/js/vite.config.js b/flutter/web/v1/js/vite.config.js deleted file mode 100644 index 22c51fa54..000000000 --- a/flutter/web/v1/js/vite.config.js +++ /dev/null @@ -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]`, - } - } - }, -}) \ No newline at end of file diff --git a/flutter/web/v1/js/yarn.lock b/flutter/web/v1/js/yarn.lock deleted file mode 100644 index d55664879..000000000 --- a/flutter/web/v1/js/yarn.lock +++ /dev/null @@ -1,1494 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 - -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/fs@npm:2.1.0" - dependencies: - "@gar/promisify": ^1.1.3 - semver: ^7.3.5 - checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b - languageName: node - linkType: hard - -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/move-file@npm:2.0.0" - dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 - languageName: node - linkType: hard - -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 011fe7ef0826b0fd1a95935a033a3c0fd08483903e1aa8f8b4e0704e3233406abb9ee25350ec0c20bbecb2aad8da0dcea58b392bbd77d6690736f02c143865d2 - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 67173ac34de1e242c55da52c2f5bdc65505d82453893f9b51dc74af9fe4c065cf4a657a4538e91b0d4a1a1e0a0642215e31894c31650ff6e3831471061e1ee9e - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 59240c850b1d3d0b56d8f8098dd04787dcaec5c5bd8de186fa548de86b86076e1c50e80144b90335e705a044edf5bc8b0998548474c2a10a98c7e004a1547e4b - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 0369163a3d226851682f855f81413cbf166cd98f131edb94a0f67f79e75342d86e89df9d7a1df08ac28be2bc77e0a7f0200526bb6c2a407abbfee1f0262d5fd7 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" - dependencies: - "@protobufjs/aspromise": ^1.1.1 - "@protobufjs/inquire": ^1.1.0 - checksum: 3fce7e09eb3f1171dd55a192066450f65324fd5f7cc01a431df01bb00d0a895e6bfb5b0c5561ce157ee1d886349c90703d10a4e11a1a256418ff591b969b3477 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 5781e1241270b8bd1591d324ca9e3a3128d2f768077a446187a049e36505e91bc4156ed5ac3159c3ce3d2ba3743dbc757b051b2d723eea9cd367bfd54ab29b2f - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: ca06f02eaf65ca36fb7498fc3492b7fc087bfcc85c702bac5b86fad34b692bdce4990e0ef444c1e2aea8c034227bd1f0484be02810d5d7e931c55445555646f4 - languageName: node - linkType: hard - -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 856eeb532b16a7aac071cacde5c5620df800db4c80cee6dbc56380524736205aae21e5ae47739114bf669ab5e8ba0e767a282ad894f3b5e124197cb9224445ee - languageName: node - linkType: hard - -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: d6a34fbbd24f729e2a10ee915b74e1d77d52214de626b921b2d77288bd8f2386808da2315080f2905761527cceffe7ec34c7647bd21a5ae41a25e8212ff79451 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: f9bf3163d13aaa3b6f5e6fbf37a116e094ea021c0e1f2a7ccd0e12a29e2ce08dafba4e8b36e13f8ed7397e1591610ce880ed1289af4d66cf4ace8a36a9557278 - languageName: node - linkType: hard - -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 - languageName: node - linkType: hard - -"@types/long@npm:^4.0.1": - version: 4.0.1 - resolution: "@types/long@npm:4.0.1" - checksum: ff9653c33f5000d0f131fd98a950a0343e2e33107dd067a97ac4a3b9678e1a2e39ea44772ad920f54ef6e8f107f76bc92c2584ba905a0dc4253282a4101166d0 - languageName: node - linkType: hard - -"@types/node@npm:>=13.7.0": - version: 17.0.8 - resolution: "@types/node@npm:17.0.8" - checksum: f4cadeb9e602027520abc88c77142697e33cf6ac98bb02f8b595a398603cbd33df1f94d01c055c9f13cde0c8eaafc5e396ca72645458d42b4318b845bc7f1d0f - languageName: node - linkType: hard - -"@types/object-hash@npm:^1.3.0": - version: 1.3.4 - resolution: "@types/object-hash@npm:1.3.4" - checksum: fe4aa041427f3c69cbcf63434af6e788329b40d7208b30aa845cfc1aa6bf9b0c11b23fa33a567d85ba7f2574a95c3b4a227f4b9b9b55da1eaea68ab94b4058d9 - languageName: node - linkType: hard - -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 - languageName: node - linkType: hard - -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: 4 - checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d - languageName: node - linkType: hard - -"agentkeepalive@npm:^4.2.1": - version: 4.2.1 - resolution: "agentkeepalive@npm:4.2.1" - dependencies: - debug: ^4.1.0 - depd: ^1.1.2 - humanize-ms: ^1.2.1 - checksum: 39cb49ed8cf217fd6da058a92828a0a84e0b74c35550f82ee0a10e1ee403c4b78ade7948be2279b188b7a7303f5d396ea2738b134731e464bf28de00a4f72a18 - languageName: node - linkType: hard - -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: ^2.0.0 - indent-string: ^4.0.0 - checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 - languageName: node - linkType: hard - -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b - languageName: node - linkType: hard - -"aproba@npm:^1.0.3 || ^2.0.0": - version: 2.0.0 - resolution: "aproba@npm:2.0.0" - checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 - languageName: node - linkType: hard - -"are-we-there-yet@npm:^3.0.0": - version: 3.0.0 - resolution: "are-we-there-yet@npm:3.0.0" - dependencies: - delegates: ^1.0.0 - readable-stream: ^3.6.0 - checksum: 348edfdd931b0b50868b55402c01c3f64df1d4c229ab6f063539a5025fd6c5f5bb8a0cab409bbed8d75d34762d22aa91b7c20b4204eb8177063158d9ba792981 - languageName: node - linkType: hard - -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: ^1.0.0 - concat-map: 0.0.1 - checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 - languageName: node - linkType: hard - -"brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" - dependencies: - balanced-match: ^1.0.0 - checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 - languageName: node - linkType: hard - -"cacache@npm:^16.0.2": - version: 16.0.7 - resolution: "cacache@npm:16.0.7" - dependencies: - "@npmcli/fs": ^2.1.0 - "@npmcli/move-file": ^2.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - glob: ^8.0.1 - infer-owner: ^1.0.4 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - mkdirp: ^1.0.4 - p-map: ^4.0.0 - promise-inflight: ^1.0.1 - rimraf: ^3.0.2 - ssri: ^9.0.0 - tar: ^6.1.11 - unique-filename: ^1.1.1 - checksum: 2155b099b7e0f0369fb1155ca4673532ca7efe2ebdbec63acca8743580b8446b5d4fd7184626b1cb059001af77b981cdc67035c7855544d365d4f048eafca2ca - languageName: node - linkType: hard - -"chownr@npm:^2.0.0": - version: 2.0.0 - resolution: "chownr@npm:2.0.0" - checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f - languageName: node - linkType: hard - -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 - languageName: node - linkType: hard - -"color-support@npm:^1.1.3": - version: 1.1.3 - resolution: "color-support@npm:1.1.3" - bin: - color-support: bin.js - checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b - languageName: node - linkType: hard - -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af - languageName: node - linkType: hard - -"console-control-strings@npm:^1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed - languageName: node - linkType: hard - -"dataloader@npm:^1.4.0": - version: 1.4.0 - resolution: "dataloader@npm:1.4.0" - checksum: e2c93d43afde68980efc0cd9ff48e9851116e27a9687f863e02b56d46f7e7868cc762cd6dcbaf4197e1ca850a03651510c165c2ae24b8e9843fd894002ad0e20 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3": - version: 4.3.4 - resolution: "debug@npm:4.3.4" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 - languageName: node - linkType: hard - -"delegates@npm:^1.0.0": - version: 1.0.0 - resolution: "delegates@npm:1.0.0" - checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd - languageName: node - linkType: hard - -"depd@npm:^1.1.2": - version: 1.1.2 - resolution: "depd@npm:1.1.2" - checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 - languageName: node - linkType: hard - -"emoji-regex@npm:^8.0.0": - version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" - checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 - languageName: node - linkType: hard - -"encoding@npm:^0.1.13": - version: 0.1.13 - resolution: "encoding@npm:0.1.13" - dependencies: - iconv-lite: ^0.6.2 - checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e - languageName: node - linkType: hard - -"err-code@npm:^2.0.2": - version: 2.0.3 - resolution: "err-code@npm:2.0.3" - checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 - languageName: node - linkType: hard - -"esbuild-android-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-android-arm64@npm:0.13.15" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-darwin-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-darwin-64@npm:0.13.15" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"esbuild-darwin-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-darwin-arm64@npm:0.13.15" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-freebsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-freebsd-64@npm:0.13.15" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-freebsd-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-freebsd-arm64@npm:0.13.15" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-32@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-32@npm:0.13.15" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-linux-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-64@npm:0.13.15" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"esbuild-linux-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-arm64@npm:0.13.15" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-arm@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-arm@npm:0.13.15" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"esbuild-linux-mips64le@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-mips64le@npm:0.13.15" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"esbuild-linux-ppc64le@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-ppc64le@npm:0.13.15" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"esbuild-netbsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-netbsd-64@npm:0.13.15" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-openbsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-openbsd-64@npm:0.13.15" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-sunos-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-sunos-64@npm:0.13.15" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-32@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-32@npm:0.13.15" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-windows-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-64@npm:0.13.15" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-arm64@npm:0.13.15" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"esbuild@npm:^0.13.12": - version: 0.13.15 - resolution: "esbuild@npm:0.13.15" - dependencies: - esbuild-android-arm64: 0.13.15 - esbuild-darwin-64: 0.13.15 - esbuild-darwin-arm64: 0.13.15 - esbuild-freebsd-64: 0.13.15 - esbuild-freebsd-arm64: 0.13.15 - esbuild-linux-32: 0.13.15 - esbuild-linux-64: 0.13.15 - esbuild-linux-arm: 0.13.15 - esbuild-linux-arm64: 0.13.15 - esbuild-linux-mips64le: 0.13.15 - esbuild-linux-ppc64le: 0.13.15 - esbuild-netbsd-64: 0.13.15 - esbuild-openbsd-64: 0.13.15 - esbuild-sunos-64: 0.13.15 - esbuild-windows-32: 0.13.15 - esbuild-windows-64: 0.13.15 - esbuild-windows-arm64: 0.13.15 - dependenciesMeta: - esbuild-android-arm64: - optional: true - esbuild-darwin-64: - optional: true - esbuild-darwin-arm64: - optional: true - esbuild-freebsd-64: - optional: true - esbuild-freebsd-arm64: - optional: true - esbuild-linux-32: - optional: true - esbuild-linux-64: - optional: true - esbuild-linux-arm: - optional: true - esbuild-linux-arm64: - optional: true - esbuild-linux-mips64le: - optional: true - esbuild-linux-ppc64le: - optional: true - esbuild-netbsd-64: - optional: true - esbuild-openbsd-64: - optional: true - esbuild-sunos-64: - optional: true - esbuild-windows-32: - optional: true - esbuild-windows-64: - optional: true - esbuild-windows-arm64: - optional: true - bin: - esbuild: bin/esbuild - checksum: d5fac8f28a6328592e45f9d49a7e98420cf2c2a3ff5a753bbf011ab79bcb5c062209ef862d3ae0875d8f2a50d40c112b0224e8b419a7cbffc6e2b02cee11ef7e - languageName: node - linkType: hard - -"fast-sha256@npm:^1.3.0": - version: 1.3.0 - resolution: "fast-sha256@npm:1.3.0" - checksum: 2b0bea7d3a9955e67abd2d3fbef4ce57f7dbb75708fc206d14973bd1d97aaf35b5c0a59c1d65be6f755df43d73b7657b9eac4fb3c2d58e6849966db1ef1fa186 - languageName: node - linkType: hard - -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": - version: 2.1.0 - resolution: "fs-minipass@npm:2.1.0" - dependencies: - minipass: ^3.0.0 - checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 - languageName: node - linkType: hard - -"fsevents@npm:~2.3.2": - version: 2.3.2 - resolution: "fsevents@npm:2.3.2" - dependencies: - node-gyp: latest - checksum: 97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@~2.3.2#~builtin": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a - languageName: node - linkType: hard - -"gauge@npm:^4.0.3": - version: 4.0.4 - resolution: "gauge@npm:4.0.4" - dependencies: - aproba: ^1.0.3 || ^2.0.0 - color-support: ^1.1.3 - console-control-strings: ^1.1.0 - has-unicode: ^2.0.1 - signal-exit: ^3.0.7 - string-width: ^4.2.3 - strip-ansi: ^6.0.1 - wide-align: ^1.1.5 - checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d - languageName: node - linkType: hard - -"glob@npm:^7.1.3, glob@npm:^7.1.4": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.1.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 - languageName: node - linkType: hard - -"glob@npm:^8.0.1": - version: 8.0.3 - resolution: "glob@npm:8.0.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da - languageName: node - linkType: hard - -"has-unicode@npm:^2.0.1": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: ^1.1.1 - checksum: b9ad53d53be4af90ce5d1c38331e712522417d017d5ef1ebd0507e07c2fbad8686fffb8e12ddecd4c39ca9b9b47431afbb975b8abf7f3c3b82c98e9aad052792 - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 - languageName: node - linkType: hard - -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" - dependencies: - "@tootallnate/once": 2 - agent-base: 6 - debug: 4 - checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: 6 - debug: 4 - checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 - languageName: node - linkType: hard - -"humanize-ms@npm:^1.2.1": - version: 1.2.1 - resolution: "humanize-ms@npm:1.2.1" - dependencies: - ms: ^2.0.0 - checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.6.2": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" - dependencies: - safer-buffer: ">= 2.1.2 < 3.0.0" - checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf - languageName: node - linkType: hard - -"imurmurhash@npm:^0.1.4": - version: 0.1.4 - resolution: "imurmurhash@npm:0.1.4" - checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 - languageName: node - linkType: hard - -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 - languageName: node - linkType: hard - -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 - languageName: node - linkType: hard - -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: ^1.3.0 - wrappy: 1 - checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd - languageName: node - linkType: hard - -"inherits@npm:2, inherits@npm:^2.0.3": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 - languageName: node - linkType: hard - -"ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: a2ade53eb339fb0cbe9e69a44caab10d6e3784662285eb5d2677117ee4facc33a64679051c35e0dfdb1a3983a51ce2f5d2cb36446d52e10d01881789b76e28fb - languageName: node - linkType: hard - -"is-core-module@npm:^2.8.0": - version: 2.8.1 - resolution: "is-core-module@npm:2.8.1" - dependencies: - has: ^1.0.3 - checksum: 418b7bc10768a73c41c7ef497e293719604007f88934a6ffc5f7c78702791b8528102fb4c9e56d006d69361549b3d9519440214a74aefc7e0b79e5e4411d377f - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^3.0.0": - version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 - languageName: node - linkType: hard - -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 - languageName: node - linkType: hard - -"libsodium-wrappers@npm:^0.7.9": - version: 0.7.9 - resolution: "libsodium-wrappers@npm:0.7.9" - dependencies: - libsodium: ^0.7.0 - checksum: b5b1b9e1b4aa5662e07df244934125f9e3cd2ba7fe0ec45191a5ffc822d22f4d2f6e09e42d91c30c4f48ca0c7f810a176fdf5e32eed6722d7d82a2a719459f56 - languageName: node - linkType: hard - -"libsodium@npm:^0.7.0, libsodium@npm:^0.7.9": - version: 0.7.9 - resolution: "libsodium@npm:0.7.9" - checksum: 1c922c9972cf97ddb7207ee4f810dd291e0610dd57ea0e47f2343968392546aaa629945a2fb39ae5f19d067f6ed0bb7330f32cc9a680a847a662e9a210ce7bfb - languageName: node - linkType: hard - -"lodash@npm:^4.17.15": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 - languageName: node - linkType: hard - -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744 - languageName: node - linkType: hard - -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: ^4.0.0 - checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 - languageName: node - linkType: hard - -"lru-cache@npm:^7.7.1": - version: 7.10.1 - resolution: "lru-cache@npm:7.10.1" - checksum: e8b190d71ed0fcd7b29c71a3e9b01f851c92d1ef8865ff06b5581ca991db1e5e006920ed4da8b56da1910664ed51abfd76c46fb55e82ac252ff6c970ff910d72 - languageName: node - linkType: hard - -"make-fetch-happen@npm:^10.0.3": - version: 10.1.3 - resolution: "make-fetch-happen@npm:10.1.3" - dependencies: - agentkeepalive: ^4.2.1 - cacache: ^16.0.2 - http-cache-semantics: ^4.1.0 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 - is-lambda: ^1.0.1 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-fetch: ^2.0.3 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - negotiator: ^0.6.3 - promise-retry: ^2.0.1 - socks-proxy-agent: ^6.1.1 - ssri: ^9.0.0 - checksum: 14b9bc5fb65a1a1f53b4579c947d1ebdb18db71eb0b35a2eab612e9642a14127917528fe4ffb2c37aaa0d27dfd7507e4044e6e2e47b43985e8fa18722f535b8f - languageName: node - linkType: hard - -"minimatch@npm:^3.1.1": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: ^1.1.7 - checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a - languageName: node - linkType: hard - -"minimatch@npm:^5.0.1": - version: 5.1.0 - resolution: "minimatch@npm:5.1.0" - dependencies: - brace-expansion: ^2.0.1 - checksum: 15ce53d31a06361e8b7a629501b5c75491bc2b59712d53e802b1987121d91b433d73fcc5be92974fde66b2b51d8fb28d75a9ae900d249feb792bb1ba2a4f0a90 - languageName: node - linkType: hard - -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" - dependencies: - minipass: ^3.0.0 - checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 - languageName: node - linkType: hard - -"minipass-fetch@npm:^2.0.3": - version: 2.1.0 - resolution: "minipass-fetch@npm:2.1.0" - dependencies: - encoding: ^0.1.13 - minipass: ^3.1.6 - minipass-sized: ^1.0.3 - minizlib: ^2.1.2 - dependenciesMeta: - encoding: - optional: true - checksum: 1334732859a3f7959ed22589bafd9c40384b885aebb5932328071c33f86b3eb181d54c86919675d1825ab5f1c8e4f328878c863873258d113c29d79a4b0c9c9f - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: ^3.0.0 - checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: ^3.0.0 - checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b - languageName: node - linkType: hard - -"minipass-sized@npm:^1.0.3": - version: 1.0.3 - resolution: "minipass-sized@npm:1.0.3" - dependencies: - minipass: ^3.0.0 - checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 - languageName: node - linkType: hard - -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": - version: 3.1.6 - resolution: "minipass@npm:3.1.6" - dependencies: - yallist: ^4.0.0 - checksum: 57a04041413a3531a65062452cb5175f93383ef245d6f4a2961d34386eb9aa8ac11ac7f16f791f5e8bbaf1dfb1ef01596870c88e8822215db57aa591a5bb0a77 - languageName: node - linkType: hard - -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": - version: 2.1.2 - resolution: "minizlib@npm:2.1.2" - dependencies: - minipass: ^3.0.0 - yallist: ^4.0.0 - checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 - languageName: node - linkType: hard - -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" - bin: - mkdirp: bin/cmd.js - checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f - languageName: node - linkType: hard - -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f - languageName: node - linkType: hard - -"ms@npm:^2.0.0": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d - languageName: node - linkType: hard - -"nanoid@npm:^3.1.30": - version: 3.2.0 - resolution: "nanoid@npm:3.2.0" - bin: - nanoid: bin/nanoid.cjs - checksum: 3d1d5a69fea84e538057cf64106e713931c4ef32af344068ecff153ff91252f39b0f2b472e09b0dfff43ac3cf520c92938d90e6455121fe93976e23660f4fccc - languageName: node - linkType: hard - -"negotiator@npm:^0.6.3": - version: 0.6.3 - resolution: "negotiator@npm:0.6.3" - checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 9.0.0 - resolution: "node-gyp@npm:9.0.0" - dependencies: - env-paths: ^2.2.0 - glob: ^7.1.4 - graceful-fs: ^4.2.6 - make-fetch-happen: ^10.0.3 - nopt: ^5.0.0 - npmlog: ^6.0.0 - rimraf: ^3.0.2 - semver: ^7.3.5 - tar: ^6.1.2 - which: ^2.0.2 - bin: - node-gyp: bin/node-gyp.js - checksum: 4d8ef8860f7e4f4d86c91db3f519d26ed5cc23b48fe54543e2afd86162b4acbd14f21de42a5db344525efb69a991e021b96a68c70c6e2d5f4a5cb770793da6d3 - languageName: node - linkType: hard - -"nopt@npm:^5.0.0": - version: 5.0.0 - resolution: "nopt@npm:5.0.0" - dependencies: - abbrev: 1 - bin: - nopt: bin/nopt.js - checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f - languageName: node - linkType: hard - -"npmlog@npm:^6.0.0": - version: 6.0.2 - resolution: "npmlog@npm:6.0.2" - dependencies: - are-we-there-yet: ^3.0.0 - console-control-strings: ^1.1.0 - gauge: ^4.0.3 - set-blocking: ^2.0.0 - checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a - languageName: node - linkType: hard - -"object-hash@npm:^1.3.1": - version: 1.3.1 - resolution: "object-hash@npm:1.3.1" - checksum: fdcb957a2f15a9060e30655a9f683ba1fc25dfb8809a73d32e9634bec385a2f1d686c707ac1e5f69fb773bc12df03fb64c77ce3faeed83e35f4eb1946cb1989e - languageName: node - linkType: hard - -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: 1 - checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 - languageName: node - linkType: hard - -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: ^3.0.0 - checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c - languageName: node - linkType: hard - -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a - languageName: node - linkType: hard - -"pcm-player@npm:^0.0.11": - version: 0.0.11 - resolution: "pcm-player@npm:0.0.11" - checksum: 8b02471e5788f23dbc965e577656a36c77d8a92f9481ddacac2d46c52755653e61bdb7a41698cee1a29e9df0cf8d853495b3968551b025c601ac4a7bf8139d81 - languageName: node - linkType: hard - -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 - languageName: node - linkType: hard - -"postcss@npm:^8.4.5": - version: 8.4.5 - resolution: "postcss@npm:8.4.5" - dependencies: - nanoid: ^3.1.30 - picocolors: ^1.0.0 - source-map-js: ^1.0.1 - checksum: b78abdd89c10f7b48f4bdcd376104a19d6e9c7495ab521729bdb3df315af6c211360e9f06887ad3bc0ab0f61a04b91d68ea11462997c79cced58b9ccd66fac07 - languageName: node - linkType: hard - -"prettier@npm:^2.5.1": - version: 2.5.1 - resolution: "prettier@npm:2.5.1" - bin: - prettier: bin-prettier.js - checksum: 21b9408476ea1c544b0e45d51ceb94a84789ff92095abb710942d780c862d0daebdb29972d47f6b4d0f7ebbfb0ffbf56cc2cfa3e3e9d1cca54864af185b15b66 - languageName: node - linkType: hard - -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 22749483091d2c594261517f4f80e05226d4d5ecc1fc917e1886929da56e22b5718b7f2a75f3807e7a7d471bc3be2907fe92e6e8f373ddf5c64bae35b5af3981 - languageName: node - linkType: hard - -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" - dependencies: - err-code: ^2.0.2 - retry: ^0.12.0 - checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 - languageName: node - linkType: hard - -"protobufjs@npm:^6.8.8": - version: 6.11.2 - resolution: "protobufjs@npm:6.11.2" - dependencies: - "@protobufjs/aspromise": ^1.1.2 - "@protobufjs/base64": ^1.1.2 - "@protobufjs/codegen": ^2.0.4 - "@protobufjs/eventemitter": ^1.1.0 - "@protobufjs/fetch": ^1.1.0 - "@protobufjs/float": ^1.0.2 - "@protobufjs/inquire": ^1.1.0 - "@protobufjs/path": ^1.1.2 - "@protobufjs/pool": ^1.1.0 - "@protobufjs/utf8": ^1.1.0 - "@types/long": ^4.0.1 - "@types/node": ">=13.7.0" - long: ^4.0.0 - bin: - pbjs: bin/pbjs - pbts: bin/pbts - checksum: 80e9d9610c3eb66f9eae4e44a1ae30381cedb721b7d5f635d781fe4c507e2c77bb7c879addcd1dda79733d3ae589d9e66fd18d42baf99b35df7382a0f9920795 - languageName: node - linkType: hard - -"readable-stream@npm:^3.6.0": - version: 3.6.0 - resolution: "readable-stream@npm:3.6.0" - dependencies: - inherits: ^2.0.3 - string_decoder: ^1.1.1 - util-deprecate: ^1.0.1 - checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 - languageName: node - linkType: hard - -"resolve@npm:^1.20.0": - version: 1.21.0 - resolution: "resolve@npm:1.21.0" - dependencies: - is-core-module: ^2.8.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: d7d9092a5c04a048bea16c7e5a2eb605ac3e8363a0cc5644de1fde17d5028e8d5f4343aab1d99bd327b98e91a66ea83e242718150c64dfedcb96e5e7aad6c4f5 - languageName: node - linkType: hard - -"resolve@patch:resolve@^1.20.0#~builtin": - version: 1.21.0 - resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin::version=1.21.0&hash=07638b" - dependencies: - is-core-module: ^2.8.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: a0a4d1f7409e73190f31f901f8a619960bb3bd4ae38ba3a54c7ea7e1c87758d28a73256bb8d6a35996a903d1bf14f53883f0dcac6c571c063cb8162d813ad26e - languageName: node - linkType: hard - -"retry@npm:^0.12.0": - version: 0.12.0 - resolution: "retry@npm:0.12.0" - checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c - languageName: node - linkType: hard - -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: ^7.1.3 - bin: - rimraf: bin.js - checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 - languageName: node - linkType: hard - -"rollup@npm:^2.59.0": - version: 2.64.0 - resolution: "rollup@npm:2.64.0" - dependencies: - fsevents: ~2.3.2 - dependenciesMeta: - fsevents: - optional: true - bin: - rollup: dist/bin/rollup - checksum: dc5b28538002ed635ea54af4c2ced05c52146322c61dbe0e84f294ee62e4f232a15760fdcef9bbeb742883edf9bf093ace5389bbdd816d18b9f5740555135180 - languageName: node - linkType: hard - -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 - languageName: node - linkType: hard - -"semver@npm:^7.3.5": - version: 7.3.7 - resolution: "semver@npm:7.3.7" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: 2fa3e877568cd6ce769c75c211beaed1f9fce80b28338cadd9d0b6c40f2e2862bafd62c19a6cff42f3d54292b7c623277bcab8816a2b5521cf15210d43e75232 - languageName: node - linkType: hard - -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.7": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 - languageName: node - linkType: hard - -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^6.1.1": - version: 6.2.0 - resolution: "socks-proxy-agent@npm:6.2.0" - dependencies: - agent-base: ^6.0.2 - debug: ^4.3.3 - socks: ^2.6.2 - checksum: 6723fd64fb50334e2b340fd0a80fd8488ffc5bc43d85b7cf1d25612044f814dd7d6ea417fd47602159941236f7f4bd15669fa5d7e1f852598a31288e1a43967b - languageName: node - linkType: hard - -"socks@npm:^2.6.2": - version: 2.6.2 - resolution: "socks@npm:2.6.2" - dependencies: - ip: ^1.1.5 - smart-buffer: ^4.2.0 - checksum: dd9194293059d737759d5c69273850ad4149f448426249325c4bea0e340d1cf3d266c3b022694b0dcf5d31f759de23657244c481fc1e8322add80b7985c36b5e - languageName: node - linkType: hard - -"source-map-js@npm:^1.0.1": - version: 1.0.1 - resolution: "source-map-js@npm:1.0.1" - checksum: 22606113d62bbd468712b0cb0c46e9a8629de7eb081049c62a04d977a211abafd7d61455617f8b78daba0b6c0c7e7c88f8c6b5aaeacffac0a6676ecf5caac5ce - languageName: node - linkType: hard - -"ssri@npm:^9.0.0": - version: 9.0.0 - resolution: "ssri@npm:9.0.0" - dependencies: - minipass: ^3.1.1 - checksum: bf33174232d07cc64e77ab1c51b55d28352273380c503d35642a19627e88a2c5f160039bb0a28608a353485075dda084dbf0390c7070f9f284559eb71d01b84b - languageName: node - linkType: hard - -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: ^8.0.0 - is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb - languageName: node - linkType: hard - -"string_decoder@npm:^1.1.1": - version: 1.3.0 - resolution: "string_decoder@npm:1.3.0" - dependencies: - safe-buffer: ~5.2.0 - checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 - languageName: node - linkType: hard - -"strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: ^5.0.1 - checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae - languageName: node - linkType: hard - -"tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.1.11 - resolution: "tar@npm:6.1.11" - dependencies: - chownr: ^2.0.0 - fs-minipass: ^2.0.0 - minipass: ^3.0.0 - minizlib: ^2.1.1 - mkdirp: ^1.0.3 - yallist: ^4.0.0 - checksum: a04c07bb9e2d8f46776517d4618f2406fb977a74d914ad98b264fc3db0fe8224da5bec11e5f8902c5b9bcb8ace22d95fbe3c7b36b8593b7dfc8391a25898f32f - languageName: node - linkType: hard - -"ts-poet@npm:^4.5.0": - version: 4.10.0 - resolution: "ts-poet@npm:4.10.0" - dependencies: - lodash: ^4.17.15 - prettier: ^2.5.1 - checksum: ffb3890a429f7ab59d96a7a17d9cc161bce786695af0fd77156e5779cfaeda92eaae4f15995a8c71a83cfb528d61bd2518bae19c4adec147bff8dce6f27c57d3 - languageName: node - linkType: hard - -"ts-proto-descriptors@npm:^1.2.1": - version: 1.3.1 - resolution: "ts-proto-descriptors@npm:1.3.1" - dependencies: - long: ^4.0.0 - protobufjs: ^6.8.8 - checksum: ef8acf9231375dd00cfa667c688746ae24fb8012a3875d1447cb6a6e9e0311150681719072716f58a24b1df801bcc35e56faca11ea4bac1f8146038b524b93c4 - languageName: node - linkType: hard - -"ts-proto@npm:^1.101.0": - version: 1.101.0 - resolution: "ts-proto@npm:1.101.0" - dependencies: - "@types/object-hash": ^1.3.0 - dataloader: ^1.4.0 - object-hash: ^1.3.1 - protobufjs: ^6.8.8 - ts-poet: ^4.5.0 - ts-proto-descriptors: ^1.2.1 - bin: - protoc-gen-ts_proto: protoc-gen-ts_proto - checksum: d404e34cad4fc5fb19271f7f257ff177d0ebac22ceca3b287927566a3ecda2f350b8592851d27415f6ec645525eae4ab40291ce3a6a3e151bb004478a1fe634a - languageName: node - linkType: hard - -"typescript@npm:^4.4.4": - version: 4.5.4 - resolution: "typescript@npm:4.5.4" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 59f3243f9cd6fe3161e6150ff6bf795fc843b4234a655dbd938a310515e0d61afd1ac942799e7415e4334255e41c2c49b7dd5d9fd38a17acd25a6779ca7e0961 - languageName: node - linkType: hard - -"typescript@patch:typescript@^4.4.4#~builtin": - version: 4.5.4 - resolution: "typescript@patch:typescript@npm%3A4.5.4#~builtin::version=4.5.4&hash=bda367" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: eda87927f9cfb94aca9b5e47842daf37347ad3073133e17f556fbb6c18f3493c5b551eedab0f4b26774235ddb7acbe0087250d5285f72ce6819a0891dd5a74ed - languageName: node - linkType: hard - -"unique-filename@npm:^1.1.1": - version: 1.1.1 - resolution: "unique-filename@npm:1.1.1" - dependencies: - unique-slug: ^2.0.0 - checksum: cf4998c9228cc7647ba7814e255dec51be43673903897b1786eff2ac2d670f54d4d733357eb08dea969aa5e6875d0e1bd391d668fbdb5a179744e7c7551a6f80 - languageName: node - linkType: hard - -"unique-slug@npm:^2.0.0": - version: 2.0.2 - resolution: "unique-slug@npm:2.0.2" - dependencies: - imurmurhash: ^0.1.4 - checksum: 5b6876a645da08d505dedb970d1571f6cebdf87044cb6b740c8dbb24f0d6e1dc8bdbf46825fd09f994d7cf50760e6f6e063cfa197d51c5902c00a861702eb75a - languageName: node - linkType: hard - -"util-deprecate@npm:^1.0.1": - version: 1.0.2 - resolution: "util-deprecate@npm:1.0.2" - checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 - languageName: node - linkType: hard - -"vite@npm:^2.7.2": - version: 2.7.12 - resolution: "vite@npm:2.7.12" - dependencies: - esbuild: ^0.13.12 - fsevents: ~2.3.2 - postcss: ^8.4.5 - resolve: ^1.20.0 - rollup: ^2.59.0 - peerDependencies: - less: "*" - sass: "*" - stylus: "*" - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - less: - optional: true - sass: - optional: true - stylus: - optional: true - bin: - vite: bin/vite.js - checksum: 56d62ae8131b02891f2dbd81f26a3ca28a02bfe390f9cb4e0c2d8dc831c2e2f8264dd3c45b14c7dd48e79d83d323a35148f92729e1f3385fae04fcd691f3f985 - languageName: node - linkType: hard - -"wasm-feature-detect@npm:^1.2.11": - version: 1.2.11 - resolution: "wasm-feature-detect@npm:1.2.11" - checksum: e7f28f5e6ca0722ba059e200c47a944ebd7570027a3ac5600b7178ee9bf950fe5280a68b5e3b5f29930407cc1214695ca10ea36a3d995d3445f4e34db58a8505 - languageName: node - linkType: hard - -"web_hbb@workspace:.": - version: 0.0.0-use.local - resolution: "web_hbb@workspace:." - 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 - typescript: ^4.4.4 - vite: ^2.7.2 - wasm-feature-detect: ^1.2.11 - zstddec: ^0.0.2 - languageName: unknown - linkType: soft - -"which@npm:^2.0.2": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: ^2.0.0 - bin: - node-which: ./bin/node-which - checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 - languageName: node - linkType: hard - -"wide-align@npm:^1.1.5": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: ^1.0.2 || 2 || 3 || 4 - checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 - languageName: node - linkType: hard - -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 - languageName: node - linkType: hard - -"zstddec@npm:^0.0.2": - version: 0.0.2 - resolution: "zstddec@npm:0.0.2" - checksum: 107334442a34590173cda03614006337712658fd043fa79f72bd486de527e2a16da474d7b20d4a171f086b334c2ad8a72afb634776d79bc2c36aee065babe31b - languageName: node - linkType: hard diff --git a/flutter/web/v1/libs/firebase-analytics.js b/flutter/web/v1/libs/firebase-analytics.js deleted file mode 100644 index 9b9a02b09..000000000 --- a/flutter/web/v1/libs/firebase-analytics.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("@firebase/app")):"function"==typeof define&&define.amd?define(["@firebase/app"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).firebase)}(this,function(mt){"use strict";try{!function(){function e(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=e(mt),r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var s=function(){return(s=Object.assign||function(e){for(var t,n=1,r=arguments.length;na[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function u(e,t){for(var n=0,r=t.length,i=e.length;n"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new f(o,e,i)},n);function n(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var y=/\{\$([^}]+)}/g,b=1e3,w=2,I=144e5,_=.5;function E(e,t,n){void 0===n&&(n=w);n=(t=void 0===t?b:t)*Math.pow(n,e),e=Math.round(_*n*(Math.random()-.5)*2);return Math.min(I,n+e)}var T=(S.prototype.setInstantiationMode=function(e){return this.instantiationMode=e,this},S.prototype.setMultipleInstances=function(e){return this.multipleInstances=e,this},S.prototype.setServiceProps=function(e){return this.serviceProps=e,this},S.prototype.setInstanceCreatedCallback=function(e){return this.onInstanceCreated=e,this},S);function S(e,t,n){this.name=e,this.instanceFactory=t,this.type=n,this.multipleInstances=!1,this.serviceProps={},this.instantiationMode="LAZY",this.onInstanceCreated=null}function C(n){return new Promise(function(e,t){n.onsuccess=function(){e(n.result)},n.onerror=function(){t(n.error)}})}function O(n,r,i){var o,e=new Promise(function(e,t){C(o=n[r].apply(n,i)).then(e,t)});return e.request=o,e}function N(e,n,t){t.forEach(function(t){Object.defineProperty(e.prototype,t,{get:function(){return this[n][t]},set:function(e){this[n][t]=e}})})}function D(t,n,r,e){e.forEach(function(e){e in r.prototype&&(t.prototype[e]=function(){return O(this[n],e,arguments)})})}function P(t,n,r,e){e.forEach(function(e){e in r.prototype&&(t.prototype[e]=function(){return this[n][e].apply(this[n],arguments)})})}function A(e,r,t,n){n.forEach(function(n){n in t.prototype&&(e.prototype[n]=function(){return e=this[r],(t=O(e,n,arguments)).then(function(e){if(e)return new k(e,t.request)});var e,t})})}function x(e){this._index=e}function k(e,t){this._cursor=e,this._request=t}function j(e){this._store=e}function L(n){this._tx=n,this.complete=new Promise(function(e,t){n.oncomplete=function(){e()},n.onerror=function(){t(n.error)},n.onabort=function(){t(n.error)}})}function R(e,t,n){this._db=e,this.oldVersion=t,this.transaction=new L(n)}function F(e){this._db=e}N(x,"_index",["name","keyPath","multiEntry","unique"]),D(x,"_index",IDBIndex,["get","getKey","getAll","getAllKeys","count"]),A(x,"_index",IDBIndex,["openCursor","openKeyCursor"]),N(k,"_cursor",["direction","key","primaryKey","value"]),D(k,"_cursor",IDBCursor,["update","delete"]),["advance","continue","continuePrimaryKey"].forEach(function(n){n in IDBCursor.prototype&&(k.prototype[n]=function(){var t=this,e=arguments;return Promise.resolve().then(function(){return t._cursor[n].apply(t._cursor,e),C(t._request).then(function(e){if(e)return new k(e,t._request)})})})}),j.prototype.createIndex=function(){return new x(this._store.createIndex.apply(this._store,arguments))},j.prototype.index=function(){return new x(this._store.index.apply(this._store,arguments))},N(j,"_store",["name","keyPath","indexNames","autoIncrement"]),D(j,"_store",IDBObjectStore,["put","add","delete","clear","get","getAll","getKey","getAllKeys","count"]),A(j,"_store",IDBObjectStore,["openCursor","openKeyCursor"]),P(j,"_store",IDBObjectStore,["deleteIndex"]),L.prototype.objectStore=function(){return new j(this._tx.objectStore.apply(this._tx,arguments))},N(L,"_tx",["objectStoreNames","mode"]),P(L,"_tx",IDBTransaction,["abort"]),R.prototype.createObjectStore=function(){return new j(this._db.createObjectStore.apply(this._db,arguments))},N(R,"_db",["name","version","objectStoreNames"]),P(R,"_db",IDBDatabase,["deleteObjectStore","close"]),F.prototype.transaction=function(){return new L(this._db.transaction.apply(this._db,arguments))},N(F,"_db",["name","version","objectStoreNames"]),P(F,"_db",IDBDatabase,["close"]),["openCursor","openKeyCursor"].forEach(function(i){[j,x].forEach(function(e){i in e.prototype&&(e.prototype[i.replace("open","iterate")]=function(){var e=(n=arguments,Array.prototype.slice.call(n)),t=e[e.length-1],n=this._store||this._index,r=n[i].apply(n,e.slice(0,-1));r.onsuccess=function(){t(r.result)}})})}),[x,j].forEach(function(e){e.prototype.getAll||(e.prototype.getAll=function(e,n){var r=this,i=[];return new Promise(function(t){r.iterateCursor(e,function(e){e?(i.push(e.value),void 0===n||i.length!=n?e.continue():t(i)):t(i)})})})});var M="0.4.32",B=1e4,H="w:"+M,q="FIS_v2",V="https://firebaseinstallations.googleapis.com/v1",G=36e5,K=((Re={})["missing-app-config-values"]='Missing App configuration value: "{$valueName}"',Re["not-registered"]="Firebase Installation is not registered.",Re["installation-not-found"]="Firebase Installation not found.",Re["request-failed"]='{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"',Re["app-offline"]="Could not process request. Application offline.",Re["delete-pending-registration"]="Can't delete installation while there is a pending registration request.",Re),U=new m("installations","Installations",K);function W(e){return e instanceof f&&e.code.includes("request-failed")}function $(e){e=e.projectId;return V+"/projects/"+e+"/installations"}function z(e){return{token:e.token,requestStatus:2,expiresIn:(e=e.expiresIn,Number(e.replace("s","000"))),creationTime:Date.now()}}function J(n,r){return p(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return[4,r.json()];case 1:return t=e.sent(),t=t.error,[2,U.create("request-failed",{requestName:n,serverCode:t.code,serverMessage:t.message,serverStatus:t.status})]}})})}function Y(e){e=e.apiKey;return new Headers({"Content-Type":"application/json",Accept:"application/json","x-goog-api-key":e})}function X(e,t){t=t.refreshToken,e=Y(e);return e.append("Authorization",q+" "+t),e}function Z(n){return p(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return[4,n()];case 1:return 500<=(t=e.sent()).status&&t.status<600?[2,n()]:[2,t]}})})}function Q(t){return new Promise(function(e){setTimeout(e,t)})}function ee(e){return btoa(String.fromCharCode.apply(String,u([],function(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0a[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new c(o,e,i)},v);function v(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var m=/\{\$([^}]+)}/g;function y(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function g(e,t){t=new b(e,t);return t.subscribe.bind(t)}var b=(I.prototype.next=function(t){this.forEachObserver(function(e){e.next(t)})},I.prototype.error=function(t){this.forEachObserver(function(e){e.error(t)}),this.close(t)},I.prototype.complete=function(){this.forEachObserver(function(e){e.complete()}),this.close()},I.prototype.subscribe=function(e,t,n){var r,i=this;if(void 0===e&&void 0===t&&void 0===n)throw new Error("Missing Observer.");void 0===(r=function(e,t){if("object"!=typeof e||null===e)return!1;for(var n=0,r=t;n=(null!=o?o:e.logLevel)&&a({level:R[t].toLowerCase(),message:i,args:n,type:e.name})}}(n[e])}var H=((H={})["no-app"]="No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()",H["bad-app-name"]="Illegal App name: '{$appName}",H["duplicate-app"]="Firebase App named '{$appName}' already exists",H["app-deleted"]="Firebase App named '{$appName}' already deleted",H["invalid-app-argument"]="firebase.{$appName}() takes either no argument or a Firebase App instance.",H["invalid-log-argument"]="First argument to `onLog` must be null or a function.",H),V=new d("app","Firebase",H),B="@firebase/app",M="[DEFAULT]",U=((H={})[B]="fire-core",H["@firebase/analytics"]="fire-analytics",H["@firebase/app-check"]="fire-app-check",H["@firebase/auth"]="fire-auth",H["@firebase/database"]="fire-rtdb",H["@firebase/functions"]="fire-fn",H["@firebase/installations"]="fire-iid",H["@firebase/messaging"]="fire-fcm",H["@firebase/performance"]="fire-perf",H["@firebase/remote-config"]="fire-rc",H["@firebase/storage"]="fire-gcs",H["@firebase/firestore"]="fire-fst",H["fire-js"]="fire-js",H["firebase-wrapper"]="fire-js-all",H),W=new z("@firebase/app"),G=(Object.defineProperty($.prototype,"automaticDataCollectionEnabled",{get:function(){return this.checkDestroyed_(),this.automaticDataCollectionEnabled_},set:function(e){this.checkDestroyed_(),this.automaticDataCollectionEnabled_=e},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"name",{get:function(){return this.checkDestroyed_(),this.name_},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"options",{get:function(){return this.checkDestroyed_(),this.options_},enumerable:!1,configurable:!0}),$.prototype.delete=function(){var t=this;return new Promise(function(e){t.checkDestroyed_(),e()}).then(function(){return t.firebase_.INTERNAL.removeApp(t.name_),Promise.all(t.container.getProviders().map(function(e){return e.delete()}))}).then(function(){t.isDeleted_=!0})},$.prototype._getService=function(e,t){void 0===t&&(t=M),this.checkDestroyed_();var n=this.container.getProvider(e);return n.isInitialized()||"EXPLICIT"!==(null===(e=n.getComponent())||void 0===e?void 0:e.instantiationMode)||n.initialize(),n.getImmediate({identifier:t})},$.prototype._removeServiceInstance=function(e,t){void 0===t&&(t=M),this.container.getProvider(e).clearInstance(t)},$.prototype._addComponent=function(t){try{this.container.addComponent(t)}catch(e){W.debug("Component "+t.name+" failed to register with FirebaseApp "+this.name,e)}},$.prototype._addOrOverwriteComponent=function(e){this.container.addOrOverwriteComponent(e)},$.prototype.toJSON=function(){return{name:this.name,automaticDataCollectionEnabled:this.automaticDataCollectionEnabled,options:this.options}},$.prototype.checkDestroyed_=function(){if(this.isDeleted_)throw V.create("app-deleted",{appName:this.name_})},$);function $(e,t,n){var r=this;this.firebase_=n,this.isDeleted_=!1,this.name_=t.name,this.automaticDataCollectionEnabled_=t.automaticDataCollectionEnabled||!1,this.options_=h(void 0,e),this.container=new S(t.name),this._addComponent(new O("app",function(){return r},"PUBLIC")),this.firebase_.INTERNAL.components.forEach(function(e){return r._addComponent(e)})}G.prototype.name&&G.prototype.options||G.prototype.delete||console.log("dc");var K="8.10.1";function Y(a){var s={},l=new Map,c={__esModule:!0,initializeApp:function(e,t){void 0===t&&(t={});"object"==typeof t&&null!==t||(t={name:t});var n=t;void 0===n.name&&(n.name=M);t=n.name;if("string"!=typeof t||!t)throw V.create("bad-app-name",{appName:String(t)});if(y(s,t))throw V.create("duplicate-app",{appName:t});n=new a(e,n,c);return s[t]=n},app:u,registerVersion:function(e,t,n){var r=null!==(i=U[e])&&void 0!==i?i:e;n&&(r+="-"+n);var i=r.match(/\s|\//),e=t.match(/\s|\//);i||e?(n=['Unable to register library "'+r+'" with version "'+t+'":'],i&&n.push('library name "'+r+'" contains illegal characters (whitespace or "/")'),i&&e&&n.push("and"),e&&n.push('version name "'+t+'" contains illegal characters (whitespace or "/")'),W.warn(n.join(" "))):o(new O(r+"-version",function(){return{library:r,version:t}},"VERSION"))},setLogLevel:T,onLog:function(e,t){if(null!==e&&"function"!=typeof e)throw V.create("invalid-log-argument");x(e,t)},apps:null,SDK_VERSION:K,INTERNAL:{registerComponent:o,removeApp:function(e){delete s[e]},components:l,useAsService:function(e,t){return"serverAuth"!==t?t:null}}};function u(e){if(!y(s,e=e||M))throw V.create("no-app",{appName:e});return s[e]}function o(n){var e,r=n.name;if(l.has(r))return W.debug("There were multiple attempts to register component "+r+"."),"PUBLIC"===n.type?c[r]:null;l.set(r,n),"PUBLIC"===n.type&&(e=function(e){if("function"!=typeof(e=void 0===e?u():e)[r])throw V.create("invalid-app-argument",{appName:r});return e[r]()},void 0!==n.serviceProps&&h(e,n.serviceProps),c[r]=e,a.prototype[r]=function(){for(var e=[],t=0;t 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(); \ No newline at end of file diff --git a/flutter/web/v1/yuv.wasm b/flutter/web/v1/yuv.wasm deleted file mode 100644 index f203c685c932a7c62cccb523540555965bd1a695..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8238 zcmcJUzmFuxb;ql!yXVLD3~zK?ip$%juBIixXMlzXLO1~qtSv&^^Z9U)VP{gXJW)%V zGe@3wZn>fWgR`<}*ntBD5}-f<0|6EU1qvj%I4V-}hdPyZ7*Q@0|0y<;#BQmoGd1_e=Nk)^hd76kk%R4{q}HH%~wF!t;lVzxnbDpE-PZ z@)hTwIXryrjjz16IC|ye@YOfpc;)1^2X7pn-23wVBkv}MhewOW8xIceKX~PyKDes) z`n~(SxqkWT;^@c?h7V8fy)ysnd#6V(jO_WZjvk&oSR8RYdrdaH-1^C%4fkAh>5;d8 z{(QG_N$>72cIi8=-zojm%jlDzHyr(nm?OfjV_k6>6054^eMKz`W!D$L?om)PUi7r_f!AP zW-DHgPx2|9Egv@Z{nUH28P6u}PhDW>R;=T*_v0tx6Yb}jraUp%ci(=GHm9~Zjy{(6 zo6G0bdNw-Gtn4|@DC00*xp1Iv{~`N#d+E8`V;IMpv$0FJhrV=s3^{x5FXNNROTSmu zT#Z*!kW!r*hZV8Q5aZJL_!OPZQ*rzD*wr;g;B6E?Zf|wzKI!TnxcHQ-CpET>8$SzU9L*eQR(dxW=wAR z1=908r)B-W&-r@!Nyf~Z`cq84>@Vwo=ASXHFHPvFXX&kDwYPVhekx9zaN77( z9D_NZmJgFZj)+vG;)L$^1~Z+sl;}EPc_R!O?4JH(Fl+pQEggI5?O1&6m~)MM4KV%9 zFp55gn_*DLGPX+it zOukD@%#y`44IwQmH9>irp{Xo;s~3BTO2HSbEy#X^ccph_I2;8Yn7mH({4qXgKoV zqnidquZ5kRsbuD1)ReabTv^PeA4eerpdQE2m zp178-^Z4k|qdt>G|AW&;i=6%XbF(xtC=2`Q`NehX0m>l3dd{v`kHCDAuAE9Yi|7`( zMDizaze`WePVwPI+MCr)Z^INpIC4|@s2Yyo(lbKY>70MBAMBn?eP^D|5>+g|fPi5N zvYG-eau_7AMuEYy^(rtZLU>jlhs`HVI3SkAYL`8ThpZZdQ6>6Z?9Lxhnr zSpE;>;A=HZv7!O4*x!Vl2X0fvGsMfFEA>!n%%t%pp&fXD*4lsot-XX^ip*QsSy}=n zJJTLc$it}d!llvWCN5iQG+Yi@WeqpELk5~uk&(B?KCUlS^{8OAgWTXy}$|LHp@hGbDn3STmI+f2CIE zI~%>Y{4C4ol4XgwqE<_^%&S54H0@kFqEq;0Ss@i*@5?I15Uc zHx)r8_V12|=|8o0Q?ko@_JqiaUBz8+Qr=mNp@9!-G82G5F6HN~Km{{?UQZnm1Jqi> zqjx4rMR5O}vE0WfOOBD;q=#*m2V*>G+Cu_ODRP|iipv0VE(^^#VKG0RO?!3haYp?1 z*Ty4@adpXQdol~B1)7{zbJ&PwZjJq@uN4OKh_u}zm2ck%G8IFf7pP$9v zU2|g7JIWZw-dxA>b2zF!4JUR}+rm$JX=wx0KL@s6mq78doq11}-@l zjUQFwZ2E&#%ztPkl?s347ws;Du+S>bG)Pi(02PuxE%>ApHRvean2}V}STGxra~R;$ z`&u(P!Sa2Mp`~TQl@Y785wk`bvt>!Hb}$ZW^Nl#7#hu0>m$u&($^9@pnWS;)7bsgm z`#Dmn{plbr)&6XdajfTGGL*JJcS^kqB_sHe@+<{$9^a?K9-(@KNbwfvkx%u~4W$=v zA#&;;a|&guJW->}Z8Esj$yQz#R;dW-=M9pu^lyXo&IpCD69OqiACXc4-|CKXs0y7* zIB0vO;jCA&U)Anct@g`@WMnGacU)hsO9d|b1TP)Cjc z@~s$JO@vkxunECAE4fNV8>XWD1wUOQDbu+>e0%KkZ%hH8w09QuUQ01-vV`ptaZE6m~hv49IU zhgz6pL4`S{DqKoZf(pU6+k6k?=X}%lFuAi4{n(R^<&11yp&(8_P11Jkvn+PmZG)Y+ zER^#3X4sJ^L{?-nIjEYn;*QB}`6}-caC`Lu3ya?{wbIF~mNLCfO;i%{I)4i?mbq|f zEkX=kn_?OWY`Ih;7)M8j)FuCHXvx1aVC7vT|MDl$A4-ELDMi>h6$sg^ZB?yZ+-~Digoi zppI=))!ON|wra(*u`8!e*)K2%%9*qLh}-G8mPtSM#PVzIj3K z=(mMq`TN0<>QivEB9x5;M~GLtE;Agfj1RV>`VlI*Sa=MTziaU$HJnGKZN5i$CI}T{ zEgbPk`PT*w|s-O0Oj1`yQ6 zz?6M=DdWJss?*;k5rBp|RpJpCC%RX}{Qy{f0-Q=)w|ZQM3YvQM8A;iwN4!#yEhcUaWs(t&Bncm?qo`iUMO7!t=t3H z^_Ry10D0j0ugvLc_mOkzE-}M{!OAnyCv6YeyUjQ7sH8^p$fY>LT6$cit577pxgb~( zWU)z*2u5inr$H31-Had+d6E?XBI)05f;4Zs*ls__hq*x3`6(3&%1D`7XO$PWiGmE8 zFN9D+E-|jKVytx4)y^h-S|ZMUyU<4Du^S!L5!5=u&brodKwUem*X=Am)-_tbiD@sH z*r>e;*HXHt(o(A`=<%gCk0|^_@aGJFx$x0-`K3tW!{dcdizM`W5grnpJ3dw>cCT*v z^6D+3uRS(Nqt9VVN#I9-DG6xD_-!$DKIm4Pm=fV;Od-^zn4;e>1z_c09i{}0#{jc( zYKXQ)m`wZsVT=;I^?s{TSHFz7RcRDpIBvr?eizB9n}sIb1JnvPf{bDp%}7VfjR=8X z(XKP1=t`9?n!x50;;2st0QBaE**taEc7~s-jJ&! zMxP)sG-IKc>No#1V-zI?P1qoZF!(VmipC0k)>HJgUJ7*Sl<04qlS7B8c2Mg|>!->v y?bb3aqocs#ZnJLaIvdnikH7fbm(JWbUVY(jzdiY(&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)) => { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 39f9185a0..e2838cc22 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -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 Remote { #[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 Remote { 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 Remote { 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 Remote { .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 Remote { // Start a voice call recorder, records audio and send to remote fn start_voice_call(&mut self) -> Option> { - 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 Remote { 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 Remote { 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 Remote { ); } } - 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 Remote { 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 Remote { 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::>(&pi.platform_additions) { diff --git a/src/flutter.rs b/src/flutter.rs index 298ba419b..e3c3c8c0d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -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 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d3665d7cc..5a6b66a0b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -103,6 +103,8 @@ pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn 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); diff --git a/src/ipc.rs b/src/ipc.rs index a74a0c103..0c6a8d57e 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -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, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index e03d0e282..bff726e3b 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -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(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 3f3a9310d..79eb530c4 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -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(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 7b479481f..1d90a83e2 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -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(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index bd15d2c48..c4d88fa68 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -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(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a4f7d09d2..974d2a992 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -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(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index b4a903b86..2b99704b3 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -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(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2bd617276..52641769b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -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(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d7611af0c..0db57aa7a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -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(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 464585593..9f6bcee46 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -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(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index d0ee84239..4570c8324 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -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."), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 25e2a5ec7..b454dc492 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -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(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 847bfd447..998c278cf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -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(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index eff5c6d9e..7928ebe11 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -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(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 24eac0460..ab59dfc24 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -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(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 2dacb9dd7..b4fef049b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -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(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ce4af5ac1..6e0b4f4c7 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Absence de l’autorisation de transfert de fichiers"), ("Note", "Note"), ("Connection", "Connexion"), - ("Share Screen", "Partage d’écran"), + ("Share screen", "Partage d’écran"), ("Chat", "Discussion"), ("Total", "Total"), ("items", "éléments"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Capture de l’écran"), ("Input Control", "Contrôle de la saisie"), ("Audio Capture", "Capture de l’audio"), - ("File Connection", "Connexion aux fichiers"), - ("Screen Connection", "Connexion à l’écran"), ("Do you accept?", "Acceptez-vous ?"), ("Open System Setting", "Ouvrir les paramètres système"), ("How to get Android input permission?", "Comment obtenir l’autorisation de contrôle de la saisie sur Android ?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sans étiquette"), ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("Accessible devices", "Appareils accessibles"), - ("View camera", "Afficher la caméra"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"), ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), - ("Enable camera", "Activer la caméra"), - ("No cameras", "Aucune caméra"), ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), ("Use D3D rendering", "Utiliser le rendu D3D"), ("Printer", "Imprimante"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Mot de passe à usage unique numérique"), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Afficher la caméra"), + ("Enable camera", "Activer la caméra"), + ("No cameras", "Aucune caméra"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index d8668c27f..9d83ff58a 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -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(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 3f051cf8b..fc55dd94d 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -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(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 9dd074b26..a0ea613c1 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -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(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 4c356c0d1..f4bb24734 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -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(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 245a1562a..8d287dda1 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -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(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 77addcf64..16c2097c8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -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(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b2c57d06e..efa547095 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -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(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 195f83721..8db1e737c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -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(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 19d4c0062..79b94db1c 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -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(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 37adeb2ac..be0f69345 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -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(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 46ae0e397..287661300 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -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(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 4a646251c..47aad3652 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -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(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index b7b50cf90..516e0db95 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), ("Note", "Opmerking"), ("Connection", "Verbinding"), - ("Share Screen", "Scherm Delen"), + ("Share screen", "Scherm Delen"), ("Chat", "Chat"), ("Total", "Totaal"), ("items", "items"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Schermopname"), ("Input Control", "Invoercontrole"), ("Audio Capture", "Audio Opnemen"), - ("File Connection", "Bestandsverbinding"), - ("Screen Connection", "Schermverbinding"), ("Do you accept?", "Geeft u toestemming?"), ("Open System Setting", "Systeeminstelling Openen"), ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("Accessible devices", "Toegankelijke apparaten"), - ("View camera", "Camera bekijken"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), - ("Enable camera", "Camera inschakelen"), - ("No cameras", "Geen camera's"), ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), ("Use D3D rendering", "Gebruik D3D-rendering"), ("Printer", "Printer"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Eenmalig numeriek wachtwoord"), ("Enable IPv6 P2P connection", "IPv6 P2P-verbinding inschakelen"), ("Enable UDP hole punching", "UDP-hole punching inschakelen"), + ("View camera", "Camera bekijken"), + ("Enable camera", "Camera inschakelen"), + ("No cameras", "Geen camera's"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5b79b6796..3086e9dd0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Brak uprawnień na przesyłanie plików"), ("Note", "Notatka"), ("Connection", "Połączenie"), - ("Share Screen", "Udostępnij ekran"), + ("Share screen", "Udostępnij ekran"), ("Chat", "Czat"), ("Total", "Łącznie"), ("items", "elementów"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejścia"), ("Audio Capture", "Przechwytywanie dźwięku"), - ("File Connection", "Przekazywanie plików"), - ("Screen Connection", "Przekazywanie ekranu"), ("Do you accept?", "Akceptujesz?"), ("Open System Setting", "Otwórz ustawienia systemowe"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Bez etykiety"), ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), ("Accessible devices", "Dostępne urządzenia"), - ("View camera", "Podgląd kamery"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), ("view_camera_unsupported_tip", "Zdalne urządzenie nie obsługuje podglądu kamery."), - ("Enable camera", "Włącz kamerę"), - ("No cameras", "Brak kamer"), ("d3d_render_tip", "Kiedy włączenie renderowania D3D jest włączone, ekran zdalnej kontroli może być czarny w niektórych przypadkach"), ("Use D3D rendering", "Użyj renderowania D3D"), ("Printer", "Drukarka"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Jednorazowe hasło cyfrowe"), ("Enable IPv6 P2P connection", "Włącz połączenie P2P IPv6"), ("Enable UDP hole punching", "Włącz tworzenie tunelu UDP"), + ("View camera", "Podgląd kamery"), + ("Enable camera", "Włącz kamerę"), + ("No cameras", "Brak kamer"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 8a7923159..c007c3114 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sem permissões de transferência de ficheiro"), ("Note", "Nota"), ("Connection", "Ligação"), - ("Share Screen", "Partilhar ecrã"), + ("Share screen", "Partilhar ecrã"), ("Chat", "Conversar"), ("Total", "Total"), ("items", "itens"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de Ecran"), ("Input Control", "Controle de Entrada"), ("Audio Capture", "Captura de Áudio"), - ("File Connection", "Ligação de Arquivo"), - ("Screen Connection", "Ligação de Ecran"), ("Do you accept?", "Aceita?"), ("Open System Setting", "Abrir Configurações do Sistema"), ("How to get Android input permission?", "Como activar a permissão de entrada do 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", "Ver câmara"), ("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", "Ver câmara"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5148c0e46..12199a9ac 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sem permissão para transferência de arquivo"), ("Note", "Nota"), ("Connection", "Conexão"), - ("Share Screen", "Compartilhar Tela"), + ("Share screen", "Compartilhar Tela"), ("Chat", "Chat"), ("Total", "Total"), ("items", "itens"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de Tela"), ("Input Control", "Controle de Entrada"), ("Audio Capture", "Captura de Áudio"), - ("File Connection", "Conexão de Arquivo"), - ("Screen Connection", "Conexão de Tela"), ("Do you accept?", "Você aceita?"), ("Open System Setting", "Abrir Configurações do Sistema"), ("How to get Android input permission?", "Como habilitar a permissão de entrada do Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sem etiqueta"), ("new-version-of-{}-tip", "Uma nova versão de {} está disponível"), ("Accessible devices", "Dispositivos acessíveis"), - ("View camera", "Visualizar câmera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), - ("Enable camera", "Ativar câmera"), - ("No cameras", "Sem câmeras"), ("d3d_render_tip", "Em algumas máquinas, a tela do controle remoto pode ficar preta ao usar a renderização D3D."), ("Use D3D rendering", "Usar renderização D3D"), ("Printer", "Impressora"), @@ -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", "Visualizar câmera"), + ("Enable camera", "Ativar câmera"), + ("No cameras", "Sem câmeras"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 480b14af8..8ff39d53f 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), ("Note", "Reține"), ("Connection", "Conexiune"), - ("Share Screen", "Partajează ecran"), + ("Share screen", "Partajează ecran"), ("Chat", "Mesaje"), ("Total", "Total"), ("items", "elemente"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Capturare ecran"), ("Input Control", "Control intrări"), ("Audio Capture", "Capturare audio"), - ("File Connection", "Conexiune fișier"), - ("Screen Connection", "Conexiune ecran"), ("Do you accept?", "Accepți?"), ("Open System Setting", "Deschide setări sistem"), ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe 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", "Vezi camera"), ("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", "Vezi camera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 15e156e92..b84161a89 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -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", "Использовать подключение IPv6 P2P"), ("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(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 3fc686b15..9610ca83c 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Perunu permissu pro sa tràmuda de documentos"), ("Note", "Nota"), ("Connection", "Connessione"), - ("Share Screen", "Cumpartzi ischermu"), + ("Share screen", "Cumpartzi ischermu"), ("Chat", "Tzarrada"), ("Total", "Totale"), ("items", "Elementos"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Catura de ischermu"), ("Input Control", "Controllu atziones"), ("Audio Capture", "Catura de s'àudio"), - ("File Connection", "Connessione documentos"), - ("Screen Connection", "Connessione ischermu"), ("Do you accept?", "Atzetas?"), ("Open System Setting", "Aberi sas impostatziones de sistema"), ("How to get Android input permission?", "Comente otènnere s'autorizatzione de intrada (input) in Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Chene tag"), ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("Accessible devices", "Dispositivos atzessìbiles"), - ("View camera", "Mustra sa càmera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualiza su cliente RustDesk remotu a sa versione {} o prus noa!"), ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), - ("Enable camera", "Abìlita sa càmera"), - ("No cameras", "Peruna càmera"), ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), ("Use D3D rendering", "Imprea sa renderizatzione D3D"), ("Printer", "Imprentadora"), @@ -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", "Mustra sa càmera"), + ("Enable camera", "Abìlita sa càmera"), + ("No cameras", "Peruna càmera"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 29c92bf5f..6f8968ce0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Prenos súborov nie je povolený"), ("Note", "Poznámka"), ("Connection", "Pripojenie"), - ("Share Screen", "Zdielať obrazovku"), + ("Share screen", "Zdielať obrazovku"), ("Chat", "Chat"), ("Total", "Celkom"), ("items", "položiek"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snímanie obrazovky"), ("Input Control", "Ovládanie vstupných zariadení"), ("Audio Capture", "Snímanie zvuku"), - ("File Connection", "Pripojenie súborov"), - ("Screen Connection", "Pripojenie obrazu"), ("Do you accept?", "Súhlasíte?"), ("Open System Setting", "Otvorenie nastavení systému"), ("How to get Android input permission?", "Ako v systéme Android povoliť oprávnenie písať zo vstupného zariadenia?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Zobraziť kameru"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"), ("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", "Zobraziť kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index e4ee52623..716c59a0f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ni pravic za prenos datotek"), ("Note", "Opomba"), ("Connection", "Povezava"), - ("Share Screen", "Deli zaslon"), + ("Share screen", "Deli zaslon"), ("Chat", "Pogovor"), ("Total", "Skupaj"), ("items", "elementi"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Zajem zaslona"), ("Input Control", "Nadzor vnosa"), ("Audio Capture", "Zajem zvoka"), - ("File Connection", "Datotečna povezava"), - ("Screen Connection", "Zaslonska povezava"), ("Do you accept?", "Ali sprejmete?"), ("Open System Setting", "Odpri sistemske nastavitve"), ("How to get Android input permission?", "Kako pridobiti dovoljenje za vnos na Androidu?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neoznačeno"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("Accessible devices", ""), - ("View camera", "Pogled kamere"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni 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", "Pogled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index d42be522f..a3107e867 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nuk ka leje për transferimin e dosjesve"), ("Note", "Shënime"), ("Connection", "Lidhja"), - ("Share Screen", "Ndaj ekranin"), + ("Share screen", "Ndaj ekranin"), ("Chat", "Biseda"), ("Total", "Total"), ("items", "artikuj"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Kapja e ekranit"), ("Input Control", "Kontrollo inputin"), ("Audio Capture", "Kapja e zërit"), - ("File Connection", "Lidhja e skedarëve"), - ("Screen Connection", "Lidhja e ekranit"), ("Do you accept?", "E pranoni"), ("Open System Setting", "Hapni cilësimet e sistemit"), ("How to get Android input permission?", "Si të merrni leje e inputit të 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", ""), ("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(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 374184494..f39dd00b0 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nemate pravo prenosa datoteka"), ("Note", "Primedba"), ("Connection", "Konekcija"), - ("Share Screen", "Podeli ekran"), + ("Share screen", "Podeli ekran"), ("Chat", "Dopisivanje"), ("Total", "Ukupno"), ("items", "stavki"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snimanje ekrana"), ("Input Control", "Kontrola unosa"), ("Audio Capture", "Snimanje zvuka"), - ("File Connection", "Spajanje preko datoteke"), - ("Screen Connection", "Podeli konekciju"), ("Do you accept?", "Prihvatate?"), ("Open System Setting", "Postavke otvorenog sistema"), ("How to get Android input permission?", "Kako dobiti pristup za Android unos?"), @@ -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", ""), ("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(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c5a0952da..e2e739c1f 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Rättigheter saknas"), ("Note", "Notering"), ("Connection", "Anslutning"), - ("Share Screen", "Dela skärm"), + ("Share screen", "Dela skärm"), ("Chat", "Chatt"), ("Total", "Totalt"), ("items", "föremål"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skärminspelning"), ("Input Control", "Inputkontroll"), ("Audio Capture", "Ljudinspelning"), - ("File Connection", "Fil anslutning"), - ("Screen Connection", "Skärm anslutning"), ("Do you accept?", "Accepterar du?"), ("Open System Setting", "Öppna systeminställnig"), ("How to get Android input permission?", "Hur får man Android rättigheter?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Visa 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", "Visa kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index f9ac6a3af..ece9ecf85 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -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", ""), ("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(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 72a2d0e69..6fdd8af21 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -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", ""), ("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(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1b594ec10..1317e7998 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -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(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 05ff74115..33697efeb 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Dosya aktarımı izni yok"), ("Note", "Not"), ("Connection", "Bağlantı"), - ("Share Screen", "Ekranı Paylaş"), + ("Share screen", "Ekranı Paylaş"), ("Chat", "Mesajlaş"), ("Total", "Toplam"), ("items", "öğeler"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekran görüntüsü"), ("Input Control", "Giriş Kontrolü"), ("Audio Capture", "Ses Yakalama"), - ("File Connection", "Dosya Bağlantısı"), - ("Screen Connection", "Ekran Bağlantısı"), ("Do you accept?", "Kabul ediyor musun?"), ("Open System Setting", "Sistem Ayarını Aç"), ("How to get Android input permission?", "Android giriş izni nasıl alınır?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Kamerayı görüntüle"), ("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", "Kamerayı görüntüle"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 996785206..adc55deb7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -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", "啟用 IPv6 P2P 連線"), ("Enable UDP hole punching", "啟用 UDP 打洞"), + ("View camera", "檢視相機"), + ("Enable camera", "允許查看鏡頭"), + ("No cameras", "沒有鏡頭"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6dcc90b7d..c6df72e3e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -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(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 09ab7fc17..f820766e0 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Không có quyền truyền tệp tin"), ("Note", "Ghi nhớ"), ("Connection", "Kết nối"), - ("Share Screen", "Chia sẻ màn hình"), + ("Share screen", "Chia sẻ màn hình"), ("Chat", "Chat"), ("Total", "Tổng"), ("items", "items"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ghi màn hình"), ("Input Control", "Điều khiển đầu vào"), ("Audio Capture", "Ghi âm thanh"), - ("File Connection", "Kết nối tệp tin"), - ("Screen Connection", "Kết nối màn hình"), ("Do you accept?", "Bạn có chấp nhận không?"), ("Open System Setting", "Mở cài đặt hệ thống"), ("How to get Android input permission?", "Cách để có quyền nhập trên 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", "Xem camera"), ("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", "Xem camera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 0711416fd..433bb5f36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,4 +72,4 @@ pub mod privacy_mode; #[cfg(windows)] pub mod virtual_display_manager; -mod kcp_stream; \ No newline at end of file +mod kcp_stream; diff --git a/src/server.rs b/src/server.rs index 4d70fa5fa..0dcaf7e41 100644 --- a/src/server.rs +++ b/src/server.rs @@ -33,6 +33,8 @@ use video_service::VideoSource; use crate::ipc::Data; pub mod audio_service; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod terminal_service; cfg_if::cfg_if! { if #[cfg(not(target_os = "ios"))] { mod clipboard_service; @@ -146,6 +148,7 @@ pub fn new() -> ServerPtr { } } } + // Terminal service is created per connection, not globally Arc::new(RwLock::new(server)) } diff --git a/src/server/connection.rs b/src/server/connection.rs index bdf027f93..12a3061de 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -169,6 +169,7 @@ pub enum AuthConnType { FileTransfer, PortForward, ViewCamera, + Terminal, } pub struct Connection { @@ -182,6 +183,7 @@ pub struct Connection { file_timer: crate::RustDeskInterval, file_transfer: Option<(String, bool)>, view_camera: bool, + terminal: bool, port_forward_socket: Option>, port_forward_address: String, tx_to_cm: mpsc::UnboundedSender, @@ -250,6 +252,9 @@ pub struct Connection { // For post requests that need to be sent sequentially. // eg. post_conn_audit tx_post_seq: mpsc::UnboundedSender<(String, Value)>, + terminal_service_id: String, + terminal_persistent: bool, + terminal_generic_service: Option>, } impl ConnInner { @@ -347,6 +352,7 @@ impl Connection { file_timer: crate::rustdesk_interval(time::interval(SEC30)), file_transfer: None, view_camera: false, + terminal: false, port_forward_socket: None, port_forward_address: "".to_owned(), tx_to_cm, @@ -410,6 +416,9 @@ impl Connection { tx_from_authed, printer_data: Vec::new(), tx_post_seq, + terminal_service_id: "".to_owned(), + terminal_persistent: false, + terminal_generic_service: None, }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -450,7 +459,7 @@ impl Connection { let mut last_recv_time = Instant::now(); conn.stream.set_send_timeout( - if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() { + if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() || conn.terminal { SEND_TIMEOUT_OTHER } else { SEND_TIMEOUT_VIDEO @@ -1253,6 +1262,8 @@ impl Connection { (2, AuthConnType::PortForward) } else if self.view_camera { (3, AuthConnType::ViewCamera) + } else if self.terminal { + (4, AuthConnType::Terminal) } else { (0, AuthConnType::Remote) }; @@ -1361,8 +1372,7 @@ impl Connection { return; } #[cfg(target_os = "linux")] - if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() && !self.view_camera - { + if self.is_remote() { let mut msg = "".to_string(); if crate::platform::linux::is_login_screen_wayland() { msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() @@ -1393,7 +1403,8 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.file_transfer.is_some() { - if crate::platform::is_prelogin() { // }|| self.tx_to_cm.send(ipc::Data::Test).is_err() { + if crate::platform::is_prelogin() { + // }|| self.tx_to_cm.send(ipc::Data::Test).is_err() { username = "".to_owned(); } } @@ -1408,6 +1419,8 @@ impl Connection { pi.sas_enabled = sas_enabled; pi.features = Some(Features { privacy_mode: privacy_mode::is_privacy_mode_supported(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal: true, // Terminal feature is supported on desktop only ..Default::default() }) .into(); @@ -1417,7 +1430,7 @@ impl Connection { let mut wait_session_id_confirm = false; #[cfg(windows)] self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); - if self.file_transfer.is_some() { + if self.file_transfer.is_some() || self.terminal { res.set_peer_info(pi); } else if self.view_camera { let supported_encoding = scrap::codec::Encoder::supported_encoding(); @@ -1509,6 +1522,10 @@ impl Connection { } else { self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); } + } else if self.terminal { + self.keyboard = false; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.init_terminal_service().await; } else if self.view_camera { if !wait_session_id_confirm { self.try_sub_camera_displays(); @@ -1531,9 +1548,16 @@ impl Connection { } } + #[inline] + fn is_remote(&self) -> bool { + self.file_transfer.is_none() + && self.port_forward_socket.is_none() + && !self.view_camera + && !self.terminal + } + fn try_sub_monitor_services(&mut self) { - let is_remote = - self.file_transfer.is_none() && self.port_forward_socket.is_none() && !self.view_camera; + let is_remote = self.is_remote(); if is_remote && !self.services_subed { self.services_subed = true; if let Some(s) = self.server.upgrade() { @@ -1651,6 +1675,7 @@ impl Connection { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), is_view_camera: self.view_camera, + is_terminal: self.terminal, port_forward: self.port_forward_address.clone(), peer_id, name, @@ -1902,6 +1927,19 @@ impl Connection { } self.view_camera = true; } + Some(login_request::Union::Terminal(terminal)) => { + if !Connection::permission(keys::OPTION_ENABLE_TERMINAL) { + self.send_login_error("No permission of terminal").await; + sleep(1.).await; + return false; + } + self.terminal = true; + if let Some(o) = self.options_in_login.as_ref() { + self.terminal_persistent = + o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); + } + self.terminal_service_id = terminal.service_id; + } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { self.send_login_error("No permission of IP tunneling").await; @@ -2791,7 +2829,7 @@ impl Connection { } } else if self.view_camera { self.try_sub_camera_displays(); - } else { + } else if !self.terminal { self.try_sub_monitor_services(); } } @@ -2843,6 +2881,12 @@ impl Connection { self.refresh_video_display(Some(request.display as usize)); } } + Some(message::Union::TerminalAction(action)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(self.handle_terminal_action(action).await); + #[cfg(any(target_os = "android", target_os = "ios"))] + log::warn!("Terminal action received but not supported on this platform"); + } _ => {} } } @@ -3371,6 +3415,12 @@ impl Connection { } } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.terminal_persistent.enum_value() { + if q != BoolOption::NotSet { + self.update_terminal_persistence(q == BoolOption::Yes).await; + } + } } async fn turn_on_privacy(&mut self, impl_key: String) { @@ -3562,12 +3612,7 @@ impl Connection { #[cfg(windows)] fn portable_check(&mut self) { - if self.portable.is_installed - || self.file_transfer.is_some() - || self.view_camera - || self.port_forward_socket.is_some() - || !self.keyboard - { + if self.portable.is_installed || !self.is_remote() || !self.keyboard { return; } let running = portable_client::running(); @@ -3779,6 +3824,55 @@ impl Connection { msg_out.set_message_box(res); self.send(msg_out).await; } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn update_terminal_persistence(&mut self, persistent: bool) { + self.terminal_persistent = persistent; + terminal_service::set_persistent(&self.terminal_service_id, persistent).ok(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn init_terminal_service(&mut self) { + if self.terminal_service_id.is_empty() { + self.terminal_service_id = terminal_service::generate_service_id(); + } + let s = Box::new(terminal_service::new( + self.terminal_service_id.clone(), + self.terminal_persistent, + )); + s.on_subscribe(self.inner.clone()); + self.terminal_generic_service = Some(s); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_terminal_action(&mut self, action: TerminalAction) -> ResultType<()> { + let mut proxy = terminal_service::TerminalServiceProxy::new( + self.terminal_service_id.clone(), + Some(self.terminal_persistent), + ); + + match proxy.handle_action(&action) { + Ok(Some(response)) => { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + Ok(None) => { + // No response needed + } + Err(err) => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Failed to handle action: {}", err); + response.set_error(error); + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + } + + Ok(()) + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -4151,6 +4245,10 @@ impl Drop for Connection { fn drop(&mut self) { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.release_pressed_modifiers(); + + if let Some(s) = self.terminal_generic_service.as_ref() { + s.join(); + } } } diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs new file mode 100644 index 000000000..60accd720 --- /dev/null +++ b/src/server/terminal_service.rs @@ -0,0 +1,968 @@ +use super::*; +use hbb_common::{ + anyhow::{anyhow, Context, Result}, + compress, +}; +use portable_pty::{Child, CommandBuilder, PtySize}; +use std::{ + collections::{HashMap, VecDeque}, + io::{Read, Write}, + sync::{ + mpsc::{self, Receiver, SyncSender}, + Arc, Mutex, + }, + thread, + time::{Duration, Instant}, +}; + +const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal +const MAX_BUFFER_LINES: usize = 10000; +const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services +const SERVICE_IDLE_TIMEOUT: Duration = Duration::from_secs(3600); // 1 hour idle timeout +const CHANNEL_BUFFER_SIZE: usize = 100; // Number of messages to buffer in channel +const COMPRESS_THRESHOLD: usize = 512; // Compress terminal data larger than this + +lazy_static::lazy_static! { + // Global registry of persistent terminal services indexed by service_id + static ref TERMINAL_SERVICES: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + + // Cleanup task handle + static ref CLEANUP_TASK: Arc>>> = Arc::new(Mutex::new(None)); + + // List of terminal child processes to check for zombies + static ref TERMINAL_TASKS: Arc>>> = Arc::new(Mutex::new(Vec::new())); +} + +/// Service metadata that is sent to clients +#[derive(Clone, Debug)] +pub struct ServiceMetadata { + pub service_id: String, + pub created_at: Instant, + pub terminal_count: usize, + pub is_persistent: bool, +} + +/// Generate a new persistent service ID +pub fn generate_service_id() -> String { + format!("ts_{}", uuid::Uuid::new_v4()) +} + +fn get_default_shell() -> String { + #[cfg(target_os = "windows")] + { + // Try PowerShell Core first (cross-platform version) + // Common installation paths for PowerShell Core + let pwsh_paths = [ + "pwsh.exe", + r"C:\Program Files\PowerShell\7\pwsh.exe", + r"C:\Program Files\PowerShell\6\pwsh.exe", + ]; + + for path in &pwsh_paths { + if std::path::Path::new(path).exists() { + return path.to_string(); + } + } + + // Try Windows PowerShell (should be available on all Windows systems) + let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; + if std::path::Path::new(powershell_path).exists() { + return powershell_path.to_string(); + } + + // Final fallback to cmd.exe + std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) + } + #[cfg(not(target_os = "windows"))] + { + // First try the SHELL environment variable + if let Ok(shell) = std::env::var("SHELL") { + if !shell.is_empty() { + return shell; + } + } + + // Check for common shells in order of preference + let shells = ["/bin/bash", "/bin/zsh", "/bin/sh"]; + for shell in &shells { + if std::path::Path::new(shell).exists() { + return shell.to_string(); + } + } + + // Final fallback to /bin/sh which should exist on all POSIX systems + "/bin/sh".to_string() + } +} + +/// Get or create a persistent terminal service +fn get_or_create_service( + service_id: String, + is_persistent: bool, +) -> Result>> { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + + // Check service limit + if !services.contains_key(&service_id) && services.len() >= MAX_SERVICES { + return Err(anyhow!( + "Maximum number of terminal services ({}) reached", + MAX_SERVICES + )); + } + + let service = services + .entry(service_id.clone()) + .or_insert_with(|| { + log::info!( + "Creating new terminal service: {} (persistent: {})", + service_id, + is_persistent + ); + Arc::new(Mutex::new(PersistentTerminalService::new( + service_id.clone(), + is_persistent, + ))) + }) + .clone(); + + // Ensure cleanup task is running + ensure_cleanup_task(); + + Ok(service) +} + +/// Remove a service from the global registry +fn remove_service(service_id: &str) { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + if let Some(service) = services.remove(service_id) { + log::info!("Removed service: {}", service_id); + // Close all terminals in the service + let sessions = service.lock().unwrap().sessions.clone(); + for (_, session) in sessions.iter() { + let mut session = session.lock().unwrap(); + if let Some(mut child) = session.child.take() { + // Kill the process + let _ = child.kill(); + add_to_reaper(child); + } + } + } +} + +/// List all active terminal services +pub fn list_services() -> Vec { + let services = TERMINAL_SERVICES.lock().unwrap(); + services + .iter() + .filter_map(|(id, service)| { + service.lock().ok().map(|svc| ServiceMetadata { + service_id: id.clone(), + created_at: svc.created_at, + terminal_count: svc.sessions.len(), + is_persistent: svc.is_persistent, + }) + }) + .collect() +} + +/// Get service by ID +pub fn get_service(service_id: &str) -> Option>> { + let services = TERMINAL_SERVICES.lock().unwrap(); + services.get(service_id).cloned() +} + +/// Clean up inactive services +pub fn cleanup_inactive_services() { + let services = TERMINAL_SERVICES.lock().unwrap(); + let now = Instant::now(); + let mut to_remove = Vec::new(); + + for (service_id, service) in services.iter() { + if let Ok(svc) = service.lock() { + // Remove non-persistent services after idle timeout + if !svc.is_persistent && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { + to_remove.push(service_id.clone()); + log::info!("Cleaning up idle non-persistent service: {}", service_id); + } + // Remove persistent services with no active terminals after longer timeout + else if svc.is_persistent + && svc.sessions.is_empty() + && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 + { + to_remove.push(service_id.clone()); + log::info!("Cleaning up empty persistent service: {}", service_id); + } + } + } + + // Remove outside of iteration to avoid deadlock + drop(services); + for id in to_remove { + remove_service(&id); + } +} + +/// Add a child process to the zombie reaper +fn add_to_reaper(child: Box) { + if let Ok(mut tasks) = TERMINAL_TASKS.lock() { + tasks.push(child); + } +} + +/// Check and reap zombie terminal processes +fn check_zombie_terminals() { + let mut tasks = match TERMINAL_TASKS.lock() { + Ok(t) => t, + Err(_) => return, + }; + + let mut i = 0; + while i < tasks.len() { + match tasks[i].try_wait() { + Ok(Some(_)) => { + // Process has exited, remove it + log::info!("Process exited: {:?}", tasks[i].process_id()); + tasks.remove(i); + } + Ok(None) => { + // Still running + i += 1; + } + Err(err) => { + // Error checking status, remove it + log::info!( + "Process exited with error: {:?}, err: {err}", + tasks[i].process_id() + ); + tasks.remove(i); + } + } + } +} + +/// Ensure the cleanup task is running +fn ensure_cleanup_task() { + let mut task_handle = CLEANUP_TASK.lock().unwrap(); + if task_handle.is_none() { + let handle = std::thread::spawn(|| { + log::info!("Started cleanup task"); + let mut last_service_cleanup = Instant::now(); + loop { + // Check for zombie processes every 100ms + check_zombie_terminals(); + + // Check for inactive services every 5 minutes + if last_service_cleanup.elapsed() > Duration::from_secs(300) { + cleanup_inactive_services(); + last_service_cleanup = Instant::now(); + } + + std::thread::sleep(Duration::from_millis(100)); + } + }); + *task_handle = Some(handle); + } +} + +pub fn new(service_id: String, is_persistent: bool) -> GenericService { + // Create the service with initial persistence setting + allow_err!(get_or_create_service(service_id.clone(), is_persistent)); + let svc = EmptyExtraFieldService::new(service_id.clone(), false); + GenericService::run(&svc.clone(), move |sp| run(sp, service_id.clone())); + svc.sp +} + +fn run(sp: EmptyExtraFieldService, service_id: String) -> ResultType<()> { + while sp.ok() { + let responses = TerminalServiceProxy::new(service_id.clone(), None).read_outputs(); + for response in responses { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + sp.send(msg_out); + } + + thread::sleep(Duration::from_millis(30)); // Read at ~33fps for responsive terminal + } + + // Clean up non-persistent service when loop exits + if let Some(service) = get_service(&service_id) { + let should_remove = !service.lock().unwrap().is_persistent; + if should_remove { + remove_service(&service_id); + } + } + + Ok(()) +} + +/// Output buffer for terminal session +struct OutputBuffer { + lines: VecDeque>, + total_size: usize, + last_line_incomplete: bool, +} + +impl OutputBuffer { + fn new() -> Self { + Self { + lines: VecDeque::new(), + total_size: 0, + last_line_incomplete: false, + } + } + + fn append(&mut self, data: &[u8]) { + if data.is_empty() { + return; + } + + // Handle incomplete lines + let mut start = 0; + if self.last_line_incomplete { + if let Some(last_line) = self.lines.back_mut() { + // Find first newline in new data + if let Some(newline_pos) = data.iter().position(|&b| b == b'\n') { + last_line.extend_from_slice(&data[..=newline_pos]); + start = newline_pos + 1; + self.last_line_incomplete = false; + } else { + // Still no newline, append all + last_line.extend_from_slice(data); + self.total_size += data.len(); + return; + } + } + } + + // Process remaining data + let remaining = &data[start..]; + let ends_with_newline = remaining.last() == Some(&b'\n'); + + // Split by lines + let lines: Vec<&[u8]> = remaining.split(|&b| b == b'\n').collect(); + + for (i, line) in lines.iter().enumerate() { + if i == lines.len() - 1 && !ends_with_newline && !line.is_empty() { + // Last line without newline + self.last_line_incomplete = true; + } + + if !line.is_empty() || i < lines.len() - 1 { + let mut line_data = line.to_vec(); + if i < lines.len() - 1 || ends_with_newline { + line_data.push(b'\n'); + } + + self.total_size += line_data.len(); + self.lines.push_back(line_data); + } + } + + // Trim old data if buffer is too large + while self.total_size > MAX_OUTPUT_BUFFER_SIZE || self.lines.len() > MAX_BUFFER_LINES { + if let Some(removed) = self.lines.pop_front() { + self.total_size -= removed.len(); + } + } + } + + fn get_recent(&self, max_bytes: usize) -> Vec { + let mut result = Vec::new(); + let mut size = 0; + + // Get recent lines up to max_bytes + for line in self.lines.iter().rev() { + if size + line.len() > max_bytes { + break; + } + size += line.len(); + result.splice(0..0, line.iter().cloned()); + } + + result + } +} + +pub struct TerminalSession { + pub created_at: Instant, + last_activity: Instant, + pty_pair: Option, + child: Option>, + // Channel for sending input to the writer thread + input_tx: Option>>, + // Channel for receiving output from the reader thread + output_rx: Option>>, + // Thread handles + reader_thread: Option>, + writer_thread: Option>, + output_buffer: OutputBuffer, + title: String, + pid: u32, + rows: u16, + cols: u16, + // Track if we've already sent the closed message + closed_message_sent: bool, +} + +impl TerminalSession { + fn new(terminal_id: i32, rows: u16, cols: u16) -> Self { + Self { + created_at: Instant::now(), + last_activity: Instant::now(), + pty_pair: None, + child: None, + input_tx: None, + output_rx: None, + reader_thread: None, + writer_thread: None, + output_buffer: OutputBuffer::new(), + title: format!("Terminal {}", terminal_id), + pid: 0, + rows, + cols, + closed_message_sent: false, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } +} + +impl Drop for TerminalSession { + fn drop(&mut self) { + // Drop the input channel to signal writer thread to exit + drop(self.input_tx.take()); + + // Wait for threads to finish + if let Some(writer_thread) = self.writer_thread.take() { + let _ = writer_thread.join(); + } + if let Some(reader_thread) = self.reader_thread.take() { + let _ = reader_thread.join(); + } + + // Ensure child process is properly handled when session is dropped + if let Some(mut child) = self.child.take() { + let _ = child.kill(); + add_to_reaper(child); + } + } +} + +/// Persistent terminal service that can survive connection drops +pub struct PersistentTerminalService { + service_id: String, + sessions: HashMap>>, + pub created_at: Instant, + last_activity: Instant, + pub is_persistent: bool, +} + +impl PersistentTerminalService { + pub fn new(service_id: String, is_persistent: bool) -> Self { + Self { + service_id, + sessions: HashMap::new(), + created_at: Instant::now(), + last_activity: Instant::now(), + is_persistent, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } + + /// Get list of terminal metadata + pub fn list_terminals(&self) -> Vec<(i32, String, u32, Instant)> { + self.sessions + .iter() + .map(|(id, session)| { + let s = session.lock().unwrap(); + (*id, s.title.clone(), s.pid, s.created_at) + }) + .collect() + } + + /// Get buffered output for a terminal + pub fn get_terminal_buffer(&self, terminal_id: i32, max_bytes: usize) -> Option> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + session.output_buffer.get_recent(max_bytes) + }) + } + + /// Get terminal info for recovery + pub fn get_terminal_info(&self, terminal_id: i32) -> Option<(u16, u16, Vec)> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + ( + session.rows, + session.cols, + session.output_buffer.get_recent(4096), + ) + }) + } + + /// Check if service has active terminals + pub fn has_active_terminals(&self) -> bool { + !self.sessions.is_empty() + } +} + +pub struct TerminalServiceProxy { + service_id: String, + is_persistent: bool, +} + +pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { + if let Some(service) = get_service(service_id) { + service.lock().unwrap().is_persistent = is_persistent; + Ok(()) + } else { + Err(anyhow!("Service {} not found", service_id)) + } +} + +impl TerminalServiceProxy { + pub fn new(service_id: String, is_persistent: Option) -> Self { + // Get persistence from the service if it exists + let is_persistent = + is_persistent.unwrap_or(if let Some(service) = get_service(&service_id) { + service.lock().unwrap().is_persistent + } else { + false + }); + TerminalServiceProxy { + service_id, + is_persistent, + } + } + + pub fn get_service_id(&self) -> &str { + &self.service_id + } + + pub fn handle_action(&mut self, action: &TerminalAction) -> Result> { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Terminal service {} not found", self.service_id); + response.set_error(error); + return Ok(Some(response)); + } + }; + service.lock().unwrap().update_activity(); + match &action.union { + Some(terminal_action::Union::Open(open)) => { + self.handle_open(&mut service.lock().unwrap(), open) + } + Some(terminal_action::Union::Resize(resize)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&resize.terminal_id) + .cloned(); + self.handle_resize(session, resize) + } + Some(terminal_action::Union::Data(data)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&data.terminal_id) + .cloned(); + self.handle_data(session, data) + } + Some(terminal_action::Union::Close(close)) => { + self.handle_close(&mut service.lock().unwrap(), close) + } + _ => Ok(None), + } + } + + fn handle_open( + &self, + service: &mut PersistentTerminalService, + open: &OpenTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Check if terminal already exists + if let Some(session_arc) = service.sessions.get(&open.terminal_id) { + // Reconnect to existing terminal + let session = session_arc.lock().unwrap(); + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Reconnected to existing terminal".to_string(); + opened.pid = session.pid; + // Return service_id for persistent sessions + if self.is_persistent { + opened.service_id = self.service_id.clone(); + } + response.set_opened(opened); + + // Send buffered output + let buffer = session.output_buffer.get_recent(4096); + if !buffer.is_empty() { + // We'll need to send this separately or extend the protocol + // For now, just acknowledge the reconnection + } + + return Ok(Some(response)); + } + + // Create new terminal session + log::info!( + "Creating new terminal {} for service: {}", + open.terminal_id, + service.service_id + ); + let mut session = + TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16); + + let pty_size = PtySize { + rows: open.rows as u16, + cols: open.cols as u16, + pixel_width: 0, + pixel_height: 0, + }; + + log::debug!("Opening PTY with size: {}x{}", open.rows, open.cols); + let pty_system = portable_pty::native_pty_system(); + let pty_pair = pty_system.openpty(pty_size).context("Failed to open PTY")?; + + // Use default shell for the platform + let shell = get_default_shell(); + log::debug!("Using shell: {}", shell); + let cmd = CommandBuilder::new(&shell); + + log::debug!("Spawning shell process..."); + let child = pty_pair + .slave + .spawn_command(cmd) + .context("Failed to spawn command")?; + + let writer = pty_pair + .master + .take_writer() + .context("Failed to get writer")?; + + let reader = pty_pair + .master + .try_clone_reader() + .context("Failed to get reader")?; + + session.pid = child.process_id().unwrap_or(0) as u32; + + // Create channels for input/output + let (input_tx, input_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + let (output_tx, output_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + + // Spawn writer thread + let terminal_id = open.terminal_id; + let writer_thread = thread::spawn(move || { + let mut writer = writer; + while let Ok(data) = input_rx.recv() { + if let Err(e) = writer.write_all(&data) { + log::error!("Terminal {} write error: {}", terminal_id, e); + break; + } + if let Err(e) = writer.flush() { + log::error!("Terminal {} flush error: {}", terminal_id, e); + } + } + log::debug!("Terminal {} writer thread exiting", terminal_id); + }); + + // Spawn reader thread + let terminal_id = open.terminal_id; + let reader_thread = thread::spawn(move || { + let mut reader = reader; + let mut buf = vec![0u8; 4096]; + loop { + match reader.read(&mut buf) { + Ok(0) => { + // EOF + break; + } + Ok(n) => { + let data = buf[..n].to_vec(); + // Try to send, if channel is full, drop the data + match output_tx.try_send(data) { + Ok(_) => {} + Err(mpsc::TrySendError::Full(_)) => { + log::debug!( + "Terminal {} output channel full, dropping data", + terminal_id + ); + } + Err(mpsc::TrySendError::Disconnected(_)) => { + log::debug!("Terminal {} output channel disconnected", terminal_id); + break; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // For non-blocking I/O, sleep briefly + thread::sleep(Duration::from_millis(10)); + } + Err(e) => { + log::error!("Terminal {} read error: {}", terminal_id, e); + break; + } + } + } + log::debug!("Terminal {} reader thread exiting", terminal_id); + }); + + session.pty_pair = Some(pty_pair); + session.child = Some(child); + session.input_tx = Some(input_tx); + session.output_rx = Some(output_rx); + session.reader_thread = Some(reader_thread); + session.writer_thread = Some(writer_thread); + + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Terminal opened".to_string(); + opened.pid = session.pid; + // Return service_id for persistent sessions + if self.is_persistent { + opened.service_id = service.service_id.clone(); + } + response.set_opened(opened); + + log::info!( + "Terminal {} opened successfully with PID {}", + open.terminal_id, + session.pid + ); + + // Store the session + service + .sessions + .insert(open.terminal_id, Arc::new(Mutex::new(session))); + + Ok(Some(response)) + } + + fn handle_resize( + &self, + session: Option>>, + resize: &ResizeTerminal, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + session.rows = resize.rows as u16; + session.cols = resize.cols as u16; + + if let Some(pty_pair) = &session.pty_pair { + pty_pair.master.resize(PtySize { + rows: resize.rows as u16, + cols: resize.cols as u16, + pixel_width: 0, + pixel_height: 0, + })?; + } + } + Ok(None) + } + + fn handle_data( + &self, + session: Option>>, + data: &TerminalData, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = &session.input_tx { + // Send data to writer thread + if let Err(e) = input_tx.send(data.data.to_vec()) { + log::error!( + "Failed to send data to terminal {}: {}", + data.terminal_id, + e + ); + } + } + } + + Ok(None) + } + + fn handle_close( + &self, + service: &mut PersistentTerminalService, + close: &CloseTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Always close and remove the terminal + if let Some(session_arc) = service.sessions.remove(&close.terminal_id) { + let mut session = session_arc.lock().unwrap(); + let exit_code = if let Some(mut child) = session.child.take() { + child.kill()?; + add_to_reaper(child); + -1 // -1 indicates forced termination + } else { + 0 + }; + + let mut closed = TerminalClosed::new(); + closed.terminal_id = close.terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + Ok(Some(response)) + } else { + Ok(None) + } + } + + pub fn read_outputs(&self) -> Vec { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + return vec![]; + } + }; + + // Get session references with minimal service lock time + let sessions: Vec<(i32, Arc>)> = { + let service = service.lock().unwrap(); + service + .sessions + .iter() + .map(|(id, session)| (*id, session.clone())) + .collect() + }; + + let mut responses = Vec::new(); + let mut closed_terminals = Vec::new(); + + // Process each session with its own lock + for (terminal_id, session_arc) in sessions { + if let Ok(mut session) = session_arc.try_lock() { + // Check if reader thread is still alive and we haven't sent closed message yet + let mut should_send_closed = false; + if !session.closed_message_sent { + if let Some(thread) = &session.reader_thread { + if thread.is_finished() { + should_send_closed = true; + session.closed_message_sent = true; + } + } + } + + // Read from output channel + let mut has_activity = false; + let mut received_data = Vec::new(); + if let Some(output_rx) = &session.output_rx { + // Try to read all available data + while let Ok(data) = output_rx.try_recv() { + has_activity = true; + received_data.push(data); + } + } + + // Update buffer after reading + for data in &received_data { + session.output_buffer.append(data); + } + + // Process received data for responses + for data in received_data { + let mut response = TerminalResponse::new(); + let mut terminal_data = TerminalData::new(); + terminal_data.terminal_id = terminal_id; + + // Compress data if it exceeds threshold + if data.len() > COMPRESS_THRESHOLD { + let compressed = compress::compress(&data); + if compressed.len() < data.len() { + terminal_data.data = bytes::Bytes::from(compressed); + terminal_data.compressed = true; + } else { + // Compression didn't help, send uncompressed + terminal_data.data = bytes::Bytes::from(data); + } + } else { + terminal_data.data = bytes::Bytes::from(data); + } + + response.set_data(terminal_data); + responses.push(response); + } + + if has_activity { + session.update_activity(); + } + + if should_send_closed { + closed_terminals.push(terminal_id); + } + } + } + + // Clean up closed terminals (requires service lock briefly) + if !closed_terminals.is_empty() { + let mut sessions = service.lock().unwrap().sessions.clone(); + for terminal_id in closed_terminals { + let mut exit_code = 0; + + if !self.is_persistent { + if let Some(session_arc) = sessions.remove(&terminal_id) { + service.lock().unwrap().sessions.remove(&terminal_id); + let mut session = session_arc.lock().unwrap(); + // Take the child and add to zombie reaper + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } else { + // For persistent sessions, just clear the child reference + if let Some(session_arc) = sessions.get(&terminal_id) { + let mut session = session_arc.lock().unwrap(); + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } + + let mut response = TerminalResponse::new(); + let mut closed = TerminalClosed::new(); + closed.terminal_id = terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + responses.push(response); + } + } + + responses + } + + /// Cleanup when connection drops + pub fn on_disconnect(&self) { + if !self.is_persistent { + // Remove non-persistent service + remove_service(&self.service_id); + } + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 5e56896af..57e6b37dd 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -20,6 +20,7 @@ impl InvokeUiCM for SciterHandler { client.id, client.is_file_transfer, client.is_view_camera, + client.is_terminal, client.port_forward.clone(), client.peer_id.clone(), client.name.clone(), diff --git a/src/ui/cm.tis b/src/ui/cm.tis index fc18bdfd3..479e26f92 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -29,7 +29,7 @@ class Body: Reactor.Component }; var right_style = show_chat ? "" : "display: none"; var disconnected = c.disconnected; - var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; + var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter return
@@ -48,8 +48,8 @@ class Body: Reactor.Component
- {c.is_file_transfer || c.port_forward || disconnected ? "" :
{translate('Permissions')}
} - {c.is_file_transfer || c.port_forward || disconnected ? "" :
+ {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
{translate('Permissions')}
} + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
@@ -60,6 +60,9 @@ class Body: Reactor.Component
} + {c.is_file_transfer ?
{translate('Transfer file')}
: ""} + {c.is_view_camera ?
{translate('View camera')}
: ""} + {c.is_terminal ?
{translate('Terminal')}
: ""} {c.port_forward ?
Port Forwarding: {c.port_forward}
: ""}
@@ -72,10 +75,10 @@ class Body: Reactor.Component {auth && !disconnected ? : "" } {auth && disconnected ? : "" }
- {c.is_file_transfer || c.port_forward ? "" :
{svg_chat}
} + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" :
{svg_chat}
}
- {c.is_file_transfer || c.port_forward ? "" : } + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" : }
; } @@ -356,7 +359,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -373,7 +376,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forw }); if (!name) name = "NA"; conn = { - id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, + id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, port_forward: port_forward, name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, diff --git a/src/ui/index.tis b/src/ui/index.tis index fd5058544..bee8bba8c 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -313,7 +313,9 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • {svg_checkmark}{translate('Enable clipboard')}
  • -
  • {svg_checkmark}{translate('Enable file transfer')}
  • +
  • {svg_checkmark}{translate('Enable file transfer')}
  • +
  • {svg_checkmark}{translate('Enable camera')}
  • +
  • {svg_checkmark}{translate('Enable terminal')}
  • {svg_checkmark}{translate('Enable remote restart')}
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • {is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 82cac19b1..f99e2de6e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -313,16 +313,10 @@ impl InvokeUiSession for SciterHandler { fn on_connected(&self, conn_type: ConnType) { match conn_type { - ConnType::RDP => {} - ConnType::PORT_FORWARD => {} - ConnType::FILE_TRANSFER => {} - ConnType::VIEW_CAMERA => {} ConnType::DEFAULT_CONN => { crate::keyboard::client::start_grab_loop(); } - // Left empty code from compilation. - // Please replace the code in the PR. - ConnType::VIEW_CAMERA => {} + _ => {} } } @@ -387,6 +381,11 @@ impl InvokeUiSession for SciterHandler { fn handle_screenshot_resp(&self, _sid: String, msg: String) { self.call("screenshot", &make_args!(msg)); } + + fn handle_terminal_response(&self, _response: TerminalResponse) { + // Terminal support is not implemented for Sciter UI + // This is a stub implementation to satisfy the trait requirements + } } pub struct SciterSession(Session); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 1da297a4e..880f0ca61 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -48,6 +48,7 @@ pub struct Client { pub disconnected: bool, pub is_file_transfer: bool, pub is_view_camera: bool, + pub is_terminal: bool, pub port_forward: String, pub name: String, pub peer_id: String, @@ -130,6 +131,7 @@ impl ConnectionManager { id: i32, is_file_transfer: bool, is_view_camera: bool, + is_terminal: bool, port_forward: String, peer_id: String, name: String, @@ -150,6 +152,7 @@ impl ConnectionManager { disconnected: false, is_file_transfer, is_view_camera, + is_terminal, port_forward, name: name.clone(), peer_id: peer_id.clone(), @@ -206,7 +209,7 @@ impl ConnectionManager { .read() .unwrap() .iter() - .filter(|(_k, v)| !v.is_file_transfer) + .filter(|(_k, v)| !v.is_file_transfer && !v.is_terminal) .next() .is_none() { @@ -405,9 +408,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -676,6 +679,7 @@ pub async fn start_listen( id, is_file_transfer, is_view_camera, + is_terminal, port_forward, peer_id, name, @@ -695,6 +699,7 @@ pub async fn start_listen( id, is_file_transfer, is_view_camera, + is_terminal, port_forward, peer_id, name, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2328a0570..93dde3909 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -20,7 +20,7 @@ use uuid::Uuid; use hbb_common::fs; use hbb_common::{ allow_err, - config::{Config, LocalConfig, PeerConfig}, + config::{keys, Config, LocalConfig, PeerConfig}, get_version_number, log, message_proto::*, rendezvous_proto::ConnType, @@ -191,10 +191,18 @@ impl Session { .eq(&ConnType::FILE_TRANSFER) } + pub fn is_default(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::DEFAULT_CONN) + } + pub fn is_view_camera(&self) -> bool { self.lc.read().unwrap().conn_type.eq(&ConnType::VIEW_CAMERA) } + pub fn is_terminal(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL) + } + pub fn is_port_forward(&self) -> bool { let conn_type = self.lc.read().unwrap().conn_type; conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP @@ -341,7 +349,7 @@ impl Session { pub fn toggle_option(&self, name: String) { let msg = self.lc.write().unwrap().toggle_option(name.clone()); #[cfg(all(target_os = "windows", not(feature = "flutter")))] - if name == hbb_common::config::keys::OPTION_ENABLE_FILE_COPY_PASTE { + if name == keys::OPTION_ENABLE_FILE_COPY_PASTE { self.send(Data::ToggleClipboardFile); } if let Some(msg) = msg { @@ -746,6 +754,57 @@ impl Session { self.send(Data::Message(msg_out)); } + // Terminal methods + pub fn open_terminal(&self, terminal_id: i32, rows: u32, cols: u32) { + let mut action = TerminalAction::new(); + action.set_open(OpenTerminal { + terminal_id, + rows, + cols, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn send_terminal_input(&self, terminal_id: i32, data: String) { + let mut action = TerminalAction::new(); + action.set_data(TerminalData { + terminal_id, + data: bytes::Bytes::from(data.into_bytes()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn resize_terminal(&self, terminal_id: i32, rows: u32, cols: u32) { + let mut action = TerminalAction::new(); + action.set_resize(ResizeTerminal { + terminal_id, + rows, + cols, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn close_terminal(&self, terminal_id: i32) { + let mut action = TerminalAction::new(); + action.set_close(CloseTerminal { + terminal_id, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn capture_displays(&self, add: Vec, sub: Vec, set: Vec) { let mut misc = Misc::new(); misc.set_capture_displays(CaptureDisplays { @@ -1488,7 +1547,7 @@ impl Session { self.read_remote_dir(remote_dir, show_hidden); } } - } else { + } else if !self.is_terminal() { self.msgbox( "success", "Successful", @@ -1603,6 +1662,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} fn printer_request(&self, id: i32, path: String); fn handle_screenshot_resp(&self, sid: String, msg: String); + fn handle_terminal_response(&self, response: TerminalResponse); } impl Deref for Session { @@ -1663,7 +1723,7 @@ impl Interface for Session { self.on_error("No active console user logged on, please connect and logon first."); return; } - } else if !self.is_port_forward() { + } else if !self.is_port_forward() && !self.is_terminal() { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); @@ -1695,7 +1755,7 @@ impl Interface for Session { self.set_peer_info(&pi); if self.is_file_transfer() { self.close_success(); - } else if !self.is_port_forward() { + } else if !self.is_port_forward() && !self.is_terminal() { self.msgbox( "success", "Successful", diff --git a/terminal.md b/terminal.md new file mode 100644 index 000000000..92a0973cb --- /dev/null +++ b/terminal.md @@ -0,0 +1,521 @@ +# RustDesk Terminal Service Implementation + +## Overview + +The RustDesk terminal service provides remote terminal/shell access with support for multiple concurrent terminal sessions per connection. It features persistence support, allowing terminal sessions to survive connection drops and be resumed later. + +## Architecture + +### Client-Side (Flutter) + +#### Terminal Connection Management +- **TerminalConnectionManager** (`flutter/lib/desktop/pages/terminal_connection_manager.dart`) + - Manages one FFI instance per peer (shared across all terminal tabs) + - Tracks persistence settings per peer + - Handles connection reference counting + +#### Terminal Models +- **TerminalModel** (`flutter/lib/models/terminal_model.dart`) + - One instance per terminal tab + - Handles terminal I/O and display using xterm package + - Manages terminal state (opened, size, buffer) + +#### UI Components +- **TerminalTabPage** (`flutter/lib/desktop/pages/terminal_tab_page.dart`) + - Manages multiple terminal tabs + - Right-click menu for persistence toggle + - Keyboard shortcuts (Cmd/Ctrl+Shift+T for new terminal) + +### Server-Side (Rust) + +#### Terminal Service Structure +```rust +TerminalService { + conn_id: i32, + service_id: String, // "tmp_{uuid}" or "persist_{uuid}" + persist: bool, +} + +PersistentTerminalService { + service_id: String, + sessions: HashMap, // terminal_id -> session + next_terminal_id: i32, + created_at: Instant, + last_activity: Instant, +} + +TerminalSession { + terminal_id: i32, + pty_pair: PtyPair, + child: Box, + writer: Box, + reader: Box, + output_buffer: OutputBuffer, // For reconnection + rows: u16, + cols: u16, +} +``` + +## Message Protocol + +### Client → Server Messages + +1. **Open Terminal** +```protobuf +TerminalAction { + open: OpenTerminal { + terminal_id: i32, + rows: u32, + cols: u32, + } +} +``` + +2. **Send Input** +```protobuf +TerminalAction { + data: TerminalData { + terminal_id: i32, + data: bytes, + } +} +``` + +3. **Resize Terminal** +```protobuf +TerminalAction { + resize: ResizeTerminal { + terminal_id: i32, + rows: u32, + cols: u32, + } +} +``` + +4. **Close Terminal** +```protobuf +TerminalAction { + close: CloseTerminal { + terminal_id: i32, + force: bool, + } +} +``` + +### Server → Client Messages + +1. **Terminal Opened** +```protobuf +TerminalResponse { + opened: TerminalOpened { + terminal_id: i32, + success: bool, + message: string, + pid: u32, + } +} +``` + +2. **Terminal Output** +```protobuf +TerminalResponse { + data: TerminalData { + terminal_id: i32, + data: bytes, // Base64 encoded in Flutter + } +} +``` + +3. **Terminal Closed** +```protobuf +TerminalResponse { + closed: TerminalClosed { + terminal_id: i32, + exit_code: i32, + } +} +``` + +## Persistence Design + +### Service ID Convention +- **Temporary**: `"tmp_{uuid}"` - Cleaned up after idle timeout +- **Persistent**: `"persist_{uuid}"` - Survives disconnections + +### Persistence Flow +1. User right-clicks terminal tab → "Enable terminal persistence" +2. Client stores persistence preference in `TerminalConnectionManager` +3. New terminals created with appropriate service ID prefix +4. Service ID saved for future reconnection (TODO: implement storage) + +### Cleanup Rules +- **Temporary services (`tmp_`)**: + - Removed after 1 hour idle time + - Immediately removed when service loop exits + +- **Persistent services**: + - Removed after 2 hours idle time IF empty + - Survive connection drops + - Can be reconnected using saved service ID + +### Cleanup Implementation + +#### 1. **Automatic Background Cleanup** +```rust +// Runs every 5 minutes +fn ensure_cleanup_task() { + tokio::spawn(async { + let mut interval = tokio::time::interval(Duration::from_secs(300)); + loop { + interval.tick().await; + cleanup_inactive_services(); + } + }); +} +``` + +#### 2. **Cleanup Logic** +```rust +fn cleanup_inactive_services() { + let now = Instant::now(); + + for (service_id, service) in services.iter() { + // Temporary services: clean up after 1 hour idle + if service_id.starts_with("tmp_") && + now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { + to_remove.push(service_id); + } + // Persistent services: clean up after 2 hours IF empty + else if !service_id.starts_with("tmp_") && + svc.sessions.is_empty() && + now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 { + to_remove.push(service_id); + } + } +} +``` + +#### 3. **Service Loop Exit Cleanup** +```rust +fn run(sp: EmptyExtraFieldService, _conn_id: i32, service_id: String) { + // Service loop + while sp.ok() { + // Read and send terminal outputs... + } + + // Clean up temporary services immediately on exit + if service_id.starts_with("tmp_") { + remove_service(&service_id); + } +} +``` + +#### 4. **Session Cleanup Within Service** +When a terminal is closed: +- PTY process is terminated +- Terminal session removed from service's HashMap +- Resources (file descriptors, buffers) are freed +- Service continues running for other terminals + +#### 5. **Connection Drop Behavior** +```rust +impl Drop for Connection { + fn drop(&mut self) { + if self.terminal { + // Unsubscribe from terminal service + server.subscribe(&service_name, self.inner.clone(), false); + } + } +} +``` +- Connection unsubscribes from service +- Service loop continues if other subscribers exist +- If no subscribers remain, `sp.ok()` returns false → service loop exits + +#### 6. **Activity Tracking** +`last_activity` is updated when: +- New terminal opened +- Input sent to terminal +- Terminal resized +- Output read from terminal +- Any terminal operation occurs + +#### 7. **Two-Phase Cleanup Process** +```rust +// Collect services to remove (while holding lock) +let mut to_remove = Vec::new(); +for (id, service) in services.iter() { + if should_remove(service) { + to_remove.push(id); + } +} + +// Remove services (after releasing lock) +drop(services); +for id in to_remove { + remove_service(&id); +} +``` +This prevents deadlock when removing services. + +## Key Features + +### Multiple Terminals per Connection +- Single FFI connection shared by all terminal tabs +- Each terminal has unique ID within the service +- Independent PTY sessions per terminal + +### Output Buffering +- Last 1MB of output buffered per terminal +- Allows showing recent history on reconnection +- Ring buffer with line-based storage + +### Cross-Platform Support +- **Unix/Linux/macOS**: Uses default shell from `$SHELL` or `/bin/bash` +- **Windows**: Uses `%COMSPEC%` or `cmd.exe` +- PTY implementation via `portable_pty` crate + +### Non-Blocking I/O +- PTY readers set to non-blocking mode (Unix) +- Output polled at ~33fps for responsive display +- Prevents blocking when no data available + +## Current Limitations + +1. **Service ID Storage**: Client doesn't persist service IDs yet +2. **Reconnection UI**: No UI to recover previous sessions +3. **Authentication**: No per-service authentication for reconnection +4. **Resource Limits**: No configurable limits on terminals per service + +## Future Enhancements + +1. **Proper Reconnection Flow**: + - Store service IDs in peer config + - UI to list and recover previous sessions + - Show buffered output on reconnection + +2. **Security**: + - Authentication token for service recovery + - Encryption of buffered output + - Access control per terminal + +3. **Advanced Features**: + - Terminal sharing between users + - Session recording/playback + - File transfer via terminal + - Custom shell/command configuration + +## Code Locations + +- **Server Implementation**: `src/server/terminal_service.rs` +- **Connection Handler**: `src/server/connection.rs` (handle_terminal_action) +- **Client Interface**: `src/ui_session_interface.rs` (terminal methods) +- **Flutter FFI**: `src/flutter_ffi.rs` (session_open_terminal, etc.) +- **Flutter Models**: `flutter/lib/models/terminal_model.dart` +- **Flutter UI**: `flutter/lib/desktop/pages/terminal_*.dart` + +## Usage + +1. **Start Terminal Session**: + - Click terminal icon or use Ctrl/Cmd+Shift+T + - Terminal opens with default shell + +2. **Enable Persistence**: + - Right-click any terminal tab + - Select "Enable terminal persistence" + - All terminals for that peer become persistent + +3. **Multiple Terminals**: + - Click "+" button or Ctrl/Cmd+Shift+T + - Each terminal is independent + +4. **Reconnection** (TODO): + - Connect to same peer + - Previous terminals automatically restored + - Recent output displayed + +## Implementation Issues & TODOs + +### Critical Missing Features + +1. **Service ID Storage & Recovery** + - Need to store service_id in peer config when persistence enabled + - Pass service_id in LoginRequest for reconnection + - Handle service_id in server login flow + - Return terminal list in LoginResponse + +2. **Protocol Extensions Needed** + ```protobuf + // In LoginRequest + message Terminal { + string service_id = 1; // For reconnection + bool persistent = 2; // Request persistence + } + + // In LoginResponse + message TerminalServiceInfo { + string service_id = 1; + repeated TerminalSessionInfo sessions = 2; + } + ``` + +3. **Terminal Recovery Flow** + - Add RecoverTerminal action to restore specific terminal + - Send buffered output on reconnection + - Handle terminal size on recovery + - UI to show available terminals + +### Current Design Issues + +1. **Service Pattern Mismatch** + - Terminal service forced into broadcast service pattern + - Should be direct connection resource, not shared service + - Complex routing through service registry unnecessary + +2. **Global State Management** + - TERMINAL_SERVICES static HashMap may cause issues + - No proper service discovery mechanism + - Cleanup task is global, not per-connection + +3. **Resource Limits Missing** + - No limit on terminals per service + - No limit on buffer size per terminal + - No limit on total services + - Could lead to resource exhaustion + +4. **Security Concerns** + - No authentication for service recovery + - Service IDs are predictable (just UUID) + - No encryption of buffered terminal output + - No access control between users + +### Performance Optimizations Needed + +1. **Output Reading** + - Currently polls at 33fps regardless of activity + - Should use event-driven I/O (epoll/kqueue) + - Batch small outputs to reduce messages + +2. **Buffer Management** + - Ring buffer could be more efficient + - Consider compression for stored output + - Implement smart truncation (keep last N complete lines) + +3. **Message Overhead** + - Each output chunk creates new protobuf message + - Could batch multiple terminal outputs + - Consider streaming protocol for continuous output + +### Platform-Specific Issues + +1. **Windows** + - ConPTY support needs testing + - Non-blocking I/O handled differently + - Shell detection could be improved + +2. **Mobile (Android/iOS)** + - Terminal feature disabled by conditional compilation + - Need to evaluate mobile terminal support + - Touch keyboard integration needed + +### Testing Requirements + +1. **Unit Tests Needed** + - Terminal service lifecycle + - Cleanup logic edge cases + - Buffer management + - Message serialization + +2. **Integration Tests** + - Multi-terminal scenarios + - Reconnection flows + - Cleanup timing + - Resource limits + +3. **Stress Tests** + - Many terminals per connection + - Large output volumes + - Rapid connect/disconnect + - Long-running sessions + +### Alternative Designs to Consider + +1. **Direct Terminal Management** + ```rust + // In Connection struct + terminals: HashMap, + + // No service pattern, direct management + async fn handle_terminal_action(&mut self, action) { + match action { + Open => self.open_terminal(), + Data => self.terminal_input(), + // etc + } + } + ``` + +2. **Actor-Based Design** + - Each terminal as an actor + - Message passing for I/O + - Better isolation and error handling + +3. **Session Manager Service** + - One global terminal manager + - Connections request terminals from manager + - Cleaner separation of concerns + +### Documentation Gaps + +1. **API Documentation** + - Document all public methods + - Add examples for common operations + - Document error conditions + +2. **Configuration** + - Document all timeouts and limits + - How to configure shell/terminal + - Platform-specific settings + +3. **Troubleshooting Guide** + - Common issues and solutions + - Debug logging interpretation + - Performance tuning + +### Future Feature Ideas + +1. **Advanced Terminal Features** + - Terminal sharing (multiple users, one terminal) + - Session recording and playback + - File transfer through terminal (zmodem) + - Custom color schemes + - Font configuration + +2. **Integration Features** + - SSH key forwarding + - Environment variable injection + - Working directory synchronization + - Shell integration (prompt markers, etc) + +3. **Management Features** + - Terminal session monitoring + - Usage statistics + - Audit logging + - Rate limiting + +### Refactoring Suggestions + +1. **Separate Concerns** + - Split terminal_service.rs into multiple files + - Separate PTY management from service logic + - Extract buffer management to own module + +2. **Improve Error Handling** + - Use proper error types, not strings + - Add error recovery mechanisms + - Better error reporting to client + +3. **Configuration Management** + - Make timeouts configurable + - Add feature flags for experimental features + - Environment-based configuration \ No newline at end of file