diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index fbaf459a4d8..54180ccdd34 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,7 +6,7 @@ on: workflow_call: env: - FLUTTER_VERSION: "3.16.9" + FLUTTER_VERSION: "3.19.6" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 74ebe72a4b6..dd21515ba54 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,8 +33,8 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.0" - NDK_VERSION: "r27" + VERSION: "1.3.2" + NDK_VERSION: "r27b" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" @@ -884,7 +884,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ libclang-10-dev \ @@ -976,8 +976,11 @@ jobs: - name: fix android for flutter 3.13 if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | - sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml - cd flutter/lib + cd flutter + sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml + sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml + flutter pub get + cd lib find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - name: Build rustdesk lib @@ -1144,7 +1147,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ libclang-10-dev \ @@ -1210,8 +1213,11 @@ jobs: - name: fix android for flutter 3.13 if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} run: | - sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml - cd flutter/lib + cd flutter + sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml + sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml + flutter pub get + cd lib find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - name: Build rustdesk @@ -1418,7 +1424,7 @@ jobs: gcc \ git \ g++ \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libclang-10-dev \ libgstreamer1.0-dev \ @@ -1675,7 +1681,7 @@ jobs: gcc \ git \ g++ \ - libappindicator3-dev \ + libayatana-appindicator3-dev \ libasound2-dev \ libclang-dev \ libdbus-1-dev \ diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 3fdcc4cfee8..efd6974a991 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.0" + VERSION: "1.3.2" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -262,7 +262,7 @@ jobs: git \ g++ \ g++-multilib \ - libappindicator3-dev \ + libayatana-appindicator3-dev\ libasound2-dev \ libc6-dev \ libclang-10-dev \ diff --git a/.gitignore b/.gitignore index 30e1aafe2f1..b4ea6266046 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ examples/**/target/ vcpkg_installed flutter/lib/generated_plugin_registrant.dart libsciter.dylib +flutter/web/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ef2cab92c80..7565d34eed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35" +source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -860,6 +860,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -3045,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.0" -source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267" +source = "git+https://github.com/rustdesk-org/hwcodec#8bbd05bb300ad07cc345356ad85570f9ea99fbfa" dependencies = [ "bindgen 0.59.2", "cc", @@ -3967,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", - "cfg_aliases", + "cfg_aliases 0.1.1", "libc", "memoffset 0.9.1", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "cfg_aliases 0.2.1", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -5187,7 +5205,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488" +source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", @@ -5462,7 +5480,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.0" +version = "1.3.2" dependencies = [ "android-wakelock", "android_logger", @@ -5494,6 +5512,7 @@ dependencies = [ "flutter_rust_bridge", "fon", "fruitbasket", + "gtk", "hbb_common", "hex", "hound", @@ -5508,6 +5527,7 @@ dependencies = [ "libpulse-simple-binding", "mac_address", "magnum-opus", + "nix 0.29.0", "num_cpus", "objc", "objc_id", @@ -5539,6 +5559,7 @@ dependencies = [ "system_shutdown", "tao", "tauri-winrt-notification", + "termios", "totp-rs", "tray-icon", "url", @@ -5559,7 +5580,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.0" +version = "1.3.2" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 5a052af0dfd..1ee749afb80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.0" +version = "1.3.2" authors = ["rustdesk "] edition = "2021" build= "build.rs" @@ -161,6 +161,9 @@ x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/ x11rb = {version = "0.12", features = ["all-extensions"], optional = true} percent-encoding = {version = "2.3", optional = true} once_cell = {version = "1.18", optional = true} +nix = { version = "0.29", features = ["term", "process"]} +gtk = "0.18" +termios = "0.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" diff --git a/README.md b/README.md index 8e2d9adb5b9..c193967d0b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

RustDesk - Your remote desktop
- Servers • + ServersBuildDockerStructure • @@ -171,3 +171,7 @@ Please ensure that you are running these commands from the root of the RustDesk ![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + +## [Public Servers](#public-servers) + +RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index c64966f2819..5ff9fc2a7b2 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.0 + version: 1.3.2 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index e1b460364b3..d8f0991cf95 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.0 + version: 1.3.2 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/build.py b/build.py index d6d05fd839a..8edd4395bfa 100755 --- a/build.py +++ b/build.py @@ -284,11 +284,14 @@ def generate_control_file(version): system2('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk +Section: net +Priority: optional Version: %s Architecture: %s Maintainer: rustdesk Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire%s +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s +Recommends: libayatana-appindicator3-1 Description: A remote control software. """ % (version, get_deb_arch(), get_deb_extra_depends()) @@ -331,8 +334,6 @@ def build_flutter_deb(version, features): 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - system2( - 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') system2( 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') system2( @@ -376,8 +377,6 @@ def build_deb_from_folder(version, binary_folder): 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - system2( - 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') system2( "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") diff --git a/docs/CODE_OF_CONDUCT-ZH.md b/docs/CODE_OF_CONDUCT-ZH.md new file mode 100644 index 00000000000..0877ab20f24 --- /dev/null +++ b/docs/CODE_OF_CONDUCT-ZH.md @@ -0,0 +1,87 @@ + +# 贡献者公约行为准则 + +## 我们的承诺 + +身为社区成员、贡献者和领袖,我们承诺使社区参与者不受骚扰,无论其年龄、体型、可见或不可见的缺陷、族裔、性征、性别认同和表达、经验水平、教育程度、社会与经济地位、国籍、相貌、种族、种姓、肤色、宗教信仰、性倾向或性取向如何。 + +我们承诺以有助于建立开放、友善、多样化、包容、健康社区的方式行事和互动。 + +## 我们的标准 + +有助于为我们的社区创造积极环境的行为例子包括但不限于: + +* 表现出对他人的同情和善意 +* 尊重不同的主张、观点和感受 +* 提出和大方接受建设性意见 +* 承担责任并向受我们错误影响的人道歉 +* 注重社区共同诉求,而非个人得失 + +不当行为例子包括: + +* 使用情色化的语言或图像,及性引诱或挑逗 +* 嘲弄、侮辱或诋毁性评论,以及人身或政治攻击 +* 公开或私下的骚扰行为 +* 未经他人明确许可,公布他人的私人信息,如物理或电子邮件地址 +* 其他有理由认定为违反职业操守的不当行为 + +## 责任和权力 + +社区领袖有责任解释和落实我们所认可的行为准则,并妥善公正地对他们认为不当、威胁、冒犯或有害的任何行为采取纠正措施。 + +社区领导有权力和责任删除、编辑或拒绝或拒绝与本行为准则不相符的评论(comment)、提交(commits)、代码、维基(wiki)编辑、议题(issues)或其他贡献,并在适当时机知采取措施的理由。 + +## 适用范围 + +本行为准则适用于所有社区场合,也适用于在公共场所代表社区时的个人。 + +代表社区的情形包括使用官方电子邮件地址、通过官方社交媒体帐户发帖或在线上或线下活动中担任指定代表。 + +## 监督 + +辱骂、骚扰或其他不可接受的行为可通过[info@rustdesk.com](mailto:info@rustdesk.com)向负责监督的社区领袖报告。 所有投诉都将得到及时和公平的审查和调查。 + +所有社区领袖都有义务尊重任何事件报告者的隐私和安全。 + +## 处理方针 + +社区领袖将遵循下列社区处理方针来明确他们所认定违反本行为准则的行为的处理方式: + +### 1. 纠正 + +**社区影响**: 使用不恰当的语言或其他在社区中被认定为不符合职业道德或不受欢迎的行为。 + +**处理意见**: 由社区领袖发出非公开的书面警告,明确说明违规行为的性质,并解释举止如何不妥。或将要求公开道歉。 + +### 2. 警告 + +**社区影响**: 单个或一系列违规行为。 + +**处理意见**: 警告并对连续性行为进行处理。在指定时间内,不得与相关人员互动,包括主动与行为准则执行者互动。这包括避免在社区场所和外部渠道中的互动。违反这些条款可能会导致临时或永久封禁。 + +### 3. 临时封禁 + +**社区影响**: 严重违反社区准则,包括持续的不当行为。 + +**处理意见**: 在指定时间内,暂时禁止与社区进行任何形式的互动或公开交流。在此期间,不得与相关人员进行公开或私下互动,包括主动与行为准则执行者互动。违反这些条款可能会导致永久封禁。 + +### 4. 永久封禁 + +**社区影响**: 行为模式表现出违反社区准则,包括持续的不当行为、骚扰个人或攻击或贬低某个类别的个体。 + +**处理意见**: 永久禁止在社区内进行任何形式的公开互动。 + +## 参见 + +本行为准则改编自[参与者公约][homepage]2.0 版, 参见 +[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]. + +指导方针借鉴自[Mozilla纪检分级][Mozilla CoC]. + +有关本行为准则的常见问题的答案,参见 [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译参见[https://www.contributor-covenant.org/translations][translations]。 + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/docs/CONTRIBUTING-ZH.md b/docs/CONTRIBUTING-ZH.md new file mode 100644 index 00000000000..718cdac69bb --- /dev/null +++ b/docs/CONTRIBUTING-ZH.md @@ -0,0 +1,32 @@ +# 为RustDesk做贡献 + +Rust欢迎每一位贡献者,如果您有意向为我们做出贡献,请遵循以下指南: + +## 贡献方式 + +对 RustDesk 或其依赖项的贡献需要通过 GitHub 的 Pull Request (PR) 的形式提交。每个 PR 都会由核心贡献者(即有权限合并代码的人)进行审核,审核通过后代码会合并到主分支,或者您会收到需要修改的反馈。所有贡献者,包括核心贡献者,提交的代码都应遵循此流程。 + +如果您希望处理某个问题,请先在对应的 GitHub issue 下发表评论,声明您将处理该问题,以避免该问题被多位贡献者重复处理。 + +## PR 注意事项 + +- 从 master 分支创建一个新的分支,并在提交PR之前,如果需要,将您的分支 变基(rebase) 到最新的 master 分支。如果您的分支无法顺利合并到 master 分支,您可能会被要求更新您的代码。 + +- 每次提交的改动应该尽可能少,并且要保证每次提交的代码都是正确的(即每个 commit 都应能成功编译并通过测试)。 + +- 每个提交都应附有开发者证书签名(http://developercertificate.org), 表明您(以及您的雇主,若适用)同意遵守项目[许可证条款](../LICENCE)。在使用 git 提交代码时,可以通过在 `git commit` 时使用 `-s` 选项加入签名 + +- 如果您的 PR 未被及时审核,或需要指定的人员进行审核,您可以通过在 PR 或评论中 @ 提到相关审核者,以及发送[电子邮件](mailto:info@rustdesk.com)的方式请求审核。 + +- 请为修复的 bug 或新增的功能添加相应的测试用例。 + +有关具体的 git 使用说明,请参考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## 行为准则 + +请遵守项目的[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。 + + +## 沟通渠道 + +RustDesk 的贡献者主要通过 [Discord](https://discord.gg/nDceKgxnkV) 进行交流。 diff --git a/docs/README-UA.md b/docs/README-UA.md index c4d2e6f9f39..8f226914d70 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -1,20 +1,18 @@

RustDesk - Ваша віддалена стільниця
- Сервери • + СервериЗбиранняDockerСтруктура • - Знімки
- [English] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
- Нам потрібна ваша допомога для перекладу цього README, інтерфейсу та документації RustDesk на вашу рідну мову + Знімки екрана
+ [English] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ Нам потрібна ваша допомога для перекладу цього README, інтерфейсу та документації RustDesk вашою рідною мовою

Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open) - Ще один застосунок для віддаленого керування стільницею, написаний на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -61,19 +59,19 @@ RustDesk вітає внесок кожного. Ознайомтеся з [CONT ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev ``` ### openSUSE Tumbleweed ```sh -sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel ``` ### Arch (Manjaro) @@ -158,18 +156,22 @@ target/release/rustdesk - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: реалізація копіювання та вставлення файлів для Windows, Linux, macOS. - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове зʼєднання +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: комунікація з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), очікування віддаленого прямого (обхід TCP NAT) або ретрансльованого зʼєднання - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для веб клієнта на Flutter + +## Знімки екрана + +![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -## Знімки +![Підключення до ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Тунелювання TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +## [Публічні сервери](#публічні-сервери) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 54b9c29a141..5a5f56b204e 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -8,7 +8,7 @@ [English] | [Українська] | [česky] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]

-Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) +与我们交流: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -18,7 +18,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](CONTRIBUTING.md). +RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md). [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) @@ -32,7 +32,9 @@ RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.m ## 依赖 -桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。 +桌面版本使用 Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简单且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。 + +请自行下载Sciter动态库。 [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | @@ -207,12 +209,13 @@ target/release/rustdesk - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows、Linux、macOS 的文件复制和粘贴实现 +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用) - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继) - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码 +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码 ## 截图 diff --git a/flutter/assets/message_24dp_5F6368.svg b/flutter/assets/message_24dp_5F6368.svg new file mode 100644 index 00000000000..5347a3d2d65 --- /dev/null +++ b/flutter/assets/message_24dp_5F6368.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 2e0a20b6db6..7f3a9cc48f4 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -302,6 +302,7 @@ prebuild) sed \ -i \ + -e 's/extended_text: .*/extended_text: 11.1.0/' \ -e 's/uni_links_desktop/#uni_links_desktop/g' \ flutter/pubspec.yaml diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 078bda8a420..a2ad9677546 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 'desktop/pages/remote_page.dart' as desktop_remote; +import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -50,6 +51,9 @@ final isLinux = isLinux_; final isDesktop = isDesktop_; final isWeb = isWeb_; final isWebDesktop = isWebDesktop_; +final isWebOnWindows = isWebOnWindows_; +final isWebOnLinux = isWebOnLinux_; +final isWebOnMacOs = isWebOnMacOS_; var isMobile = isAndroid || isIOS; var version = ''; int androidVersion = 0; @@ -347,6 +351,9 @@ class MyTheme { hoverColor: Color.fromARGB(255, 224, 224, 224), scaffoldBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white, + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), dialogTheme: DialogTheme( elevation: 15, shape: RoundedRectangleBorder( @@ -442,6 +449,9 @@ class MyTheme { hoverColor: Color.fromARGB(255, 45, 46, 53), scaffoldBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E), + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), dialogTheme: DialogTheme( elevation: 15, shape: RoundedRectangleBorder( @@ -545,9 +555,9 @@ class MyTheme { return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); } - static void changeDarkMode(ThemeMode mode) async { + static Future changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); - if (desktopType == DesktopType.main || isAndroid || isIOS) { + if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) { if (mode == ThemeMode.system) { await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: defaultOptionTheme); @@ -555,7 +565,7 @@ class MyTheme { await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - await bind.mainChangeTheme(dark: mode.toShortString()); + if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString()); // Synchronize the window theme of the system. updateSystemWindowTheme(); } @@ -671,10 +681,12 @@ closeConnection({String? id}) { overlays: SystemUiOverlay.values); gFFI.chatModel.hideChatOverlay(); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; }(); } else { if (isWeb) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; } else { final controller = Get.find(); controller.closeBy(id); @@ -1162,33 +1174,21 @@ void msgBox(SessionID sessionId, String type, String title, String text, dialogManager.dismissAll(); })); } - if (reconnect != null && title == "Connection Error") { + if (reconnect != null && + title == "Connection Error" && + reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = reconnectTimeout != null - ? Obx(() => _ReconnectCountDownButton( - second: reconnectTimeout, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - )) - : Obx( - () => dialogButton( - 'Reconnect', - isOutline: true, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - ), - ); + final button = Obx(() => _ReconnectCountDownButton( + second: reconnectTimeout, + onPressed: enabled.isTrue + ? () { + // Disable the button + enabled.value = false; + reconnect(dialogManager, sessionId, false); + } + : null, + )); buttons.insert(0, button); } if (link.isNotEmpty) { @@ -2026,6 +2026,8 @@ Future restoreWindowPosition(WindowType type, return false; } +var webInitialLink = ""; + /// Initialize uni links for macos/windows /// /// [Availability] @@ -2042,7 +2044,12 @@ Future initUniLinks() async { if (initialLink == null || initialLink.isEmpty) { return false; } - return handleUriLink(uriString: initialLink); + if (isWeb) { + webInitialLink = initialLink; + return false; + } else { + return handleUriLink(uriString: initialLink); + } } catch (err) { debugPrintStack(label: "$err"); return false; @@ -2055,7 +2062,7 @@ Future initUniLinks() async { /// /// Returns a [StreamSubscription] which can listen the uni links. StreamSubscription? listenUniLinks({handleByFlutter = true}) { - if (isLinux) { + if (isLinux || isWeb) { return null; } @@ -2285,16 +2292,19 @@ connectMainDesktop(String id, required bool isRDP, bool? forceRelay, String? password, + String? connToken, bool? isSharedPassword}) async { if (isFileTransfer) { await rustDeskWinManager.newFileTransfer(id, password: password, isSharedPassword: isSharedPassword, + connToken: connToken, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, password: password, isSharedPassword: isSharedPassword, + connToken: connToken, forceRelay: forceRelay); } else { await rustDeskWinManager.newRemoteDesktop(id, @@ -2314,6 +2324,7 @@ connect(BuildContext context, String id, bool isRDP = false, bool forceRelay = false, String? password, + String? connToken, bool? isSharedPassword}) async { if (id == '') return; if (!isDesktop || desktopType == DesktopType.main) { @@ -2355,24 +2366,40 @@ connect(BuildContext context, String id, 'password': password, 'isSharedPassword': isSharedPassword, 'forceRelay': forceRelay, + 'connToken': connToken, }); } } else { if (isFileTransfer) { - if (!await AndroidPermissionManager.check(kManageExternalStorage)) { - if (!await AndroidPermissionManager.request(kManageExternalStorage)) { - return; + if (isAndroid) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { + return; + } } } - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage( - id: id, password: password, isSharedPassword: isSharedPassword), - ), - ); + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_file_manager.FileManagerPage( + id: id, + password: password, + isSharedPassword: isSharedPassword), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage( + id: id, password: password, isSharedPassword: isSharedPassword), + ), + ); + } } else { - if (isWebDesktop) { + if (isWeb) { Navigator.push( context, MaterialPageRoute( @@ -2396,6 +2423,7 @@ connect(BuildContext context, String id, ); } } + stateGlobal.isInMainPage = false; } FocusScopeNode currentFocus = FocusScope.of(context); @@ -3145,9 +3173,13 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { importConfig(List? controllers, List? errMsgs, String? text) { + text = text?.trim(); if (text != null && text.isNotEmpty) { try { final sc = ServerConfig.decode(text); + if (isWeb || isIOS) { + sc.relayServer = ''; + } if (sc.idServer.isNotEmpty) { Future success = setServerConfig(controllers, errMsgs, sc); success.then((value) { @@ -3587,3 +3619,7 @@ List? get subWindowManagerEnableResizeEdges => isWindows SubWindowResizeEdge.topRight, ] : null; + +void earlyAssert() { + assert('\1' == '1'); +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 67b262cd11a..78bd20ef033 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; @@ -61,15 +62,16 @@ class _AddressBookState extends State { retry: null, // remove retry close: () => gFFI.abModel.currentAbPushError.value = ''), Expanded( - child: (isDesktop || isWebDesktop) - ? _buildAddressBookDesktop() - : _buildAddressBookMobile()) + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildAddressBookPortrait() + : _buildAddressBookLandscape()), + ), ], ); } }); - Widget _buildAddressBookDesktop() { + Widget _buildAddressBookLandscape() { return Row( children: [ Offstage( @@ -106,7 +108,7 @@ class _AddressBookState extends State { ); } - Widget _buildAddressBookMobile() { + Widget _buildAddressBookPortrait() { const padding = 8.0; return Column( children: [ @@ -239,14 +241,15 @@ class _AddressBookState extends State { bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); } }, - customButton: Container( - height: isDesktop ? 48 : 40, - child: Row(children: [ - Expanded( - child: buildItem(gFFI.abModel.currentName.value, button: true)), - Icon(Icons.arrow_drop_down), - ]), - ), + customButton: Obx(() => Container( + height: stateGlobal.isPortrait.isFalse ? 48 : 40, + child: Row(children: [ + Expanded( + child: + buildItem(gFFI.abModel.currentName.value, button: true)), + Icon(Icons.arrow_drop_down), + ]), + )), underline: Container( height: 0.7, color: Theme.of(context).dividerColor.withOpacity(0.1), @@ -335,8 +338,8 @@ class _AddressBookState extends State { showActionMenu: editPermission); } - final gridView = DynamicGridView.builder( - shrinkWrap: isMobile, + gridView(bool isPortrait) => DynamicGridView.builder( + shrinkWrap: isPortrait, gridDelegate: SliverGridDelegateWithWrapping(), itemCount: tags.length, itemBuilder: (BuildContext context, int index) { @@ -344,9 +347,9 @@ class _AddressBookState extends State { return tagBuilder(e); }); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); - return (isDesktop || isWebDesktop) - ? gridView - : LimitedBox(maxHeight: maxHeight, child: gridView); + return Obx(() => stateGlobal.isPortrait.isFalse + ? gridView(false) + : LimitedBox(maxHeight: maxHeight, child: gridView(true))); }); } @@ -356,7 +359,6 @@ class _AddressBookState extends State { alignment: Alignment.topLeft, child: AddressBookPeersView( menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.abModel.currentAbPeers, )), ); } @@ -506,20 +508,21 @@ class _AddressBookState extends State { double marginBottom = 4; row({required Widget lable, required Widget input}) { - return Row( - children: [ - !isMobile - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: lable.marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 200), - child: input), - ), - ], - ).marginOnly(bottom: !isMobile ? 8 : 0); + makeChild(bool isPortrait) => Row( + children: [ + !isPortrait + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: lable.marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 200), + child: input), + ), + ], + ).marginOnly(bottom: !isPortrait ? 8 : 0); + return Obx(() => makeChild(stateGlobal.isPortrait.isTrue)); } return CustomAlertDialog( @@ -542,23 +545,28 @@ class _AddressBookState extends State { ), ], ), - input: TextField( - controller: idController, - inputFormatters: [IDTextInputFormatter()], - decoration: InputDecoration( - labelText: !isMobile ? null : translate('ID'), - errorText: errorMsg, - errorMaxLines: 5), - )), + input: Obx(() => TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('ID'), + errorText: errorMsg, + errorMaxLines: 5), + ))), row( lable: Text( translate('Alias'), style: style, ), - input: TextField( - controller: aliasController, - decoration: InputDecoration( - labelText: !isMobile ? null : translate('Alias'), + input: Obx(() => TextField( + controller: aliasController, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Alias'), + ), )), ), if (isCurrentAbShared) @@ -567,22 +575,26 @@ class _AddressBookState extends State { translate('Password'), style: style, ), - input: TextField( - controller: passwordController, - obscureText: !passwordVisible, - decoration: InputDecoration( - labelText: !isMobile ? null : translate('Password'), - suffixIcon: IconButton( - icon: Icon( - passwordVisible - ? Icons.visibility - : Icons.visibility_off, - color: MyTheme.lightTheme.primaryColor), - onPressed: () { - setState(() { - passwordVisible = !passwordVisible; - }); - }, + input: Obx( + () => TextField( + controller: passwordController, + obscureText: !passwordVisible, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Password'), + suffixIcon: IconButton( + icon: Icon( + passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + }, + ), ), ), )), diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 07a11904da1..978d053df4b 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State { .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( - message: isMobile + message: !(isDesktop || isWebDesktop) ? '' : widget.peer.tags.isNotEmpty ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' diff --git a/flutter/lib/common/widgets/connection_page_title.dart b/flutter/lib/common/widgets/connection_page_title.dart new file mode 100644 index 00000000000..ba03c265696 --- /dev/null +++ b/flutter/lib/common/widgets/connection_page_title.dart @@ -0,0 +1,38 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +Widget getConnectionPageTitle(BuildContext context, bool isWeb) { + return Row( + children: [ + Expanded( + child: Row( + children: [ + AutoSizeText( + translate('Control Remote Desktop'), + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), + ).marginOnly(right: 4), + Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"), + child: Icon( + Icons.help_outline_outlined, + size: 16, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), + ), + ), + ], + )), + ], + ); +} diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart index 99ece2434bf..dafc23b448b 100644 --- a/flutter/lib/common/widgets/custom_password.dart +++ b/flutter/lib/common/widgets/custom_password.dart @@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule { String get name => translate('uppercase'); @override bool validate(String value) { - return value.contains(RegExp(r'[A-Z]')); + return value.runes.any((int rune) { + var character = String.fromCharCode(rune); + return character.toUpperCase() == character && + character.toLowerCase() != character; + }); } } @@ -24,7 +28,11 @@ class LowercaseValidationRule extends ValidationRule { @override bool validate(String value) { - return value.contains(RegExp(r'[a-z]')); + return value.runes.any((int rune) { + var character = String.fromCharCode(rune); + return character.toLowerCase() == character && + character.toUpperCase() != character; + }); } } diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 7cc76d6c6a1..cc3e0613105 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -380,6 +381,7 @@ class DialogTextField extends StatelessWidget { final FocusNode? focusNode; final TextInputType? keyboardType; final List? inputFormatters; + final int? maxLength; static const kUsernameTitle = 'Username'; static const kUsernameIcon = Icon(Icons.account_circle_outlined); @@ -397,6 +399,7 @@ class DialogTextField extends StatelessWidget { this.hintText, this.keyboardType, this.inputFormatters, + this.maxLength, required this.title, required this.controller}) : super(key: key); @@ -423,6 +426,7 @@ class DialogTextField extends StatelessWidget { obscureText: obscureText, keyboardType: keyboardType, inputFormatters: inputFormatters, + maxLength: maxLength, ), ), ], @@ -680,6 +684,7 @@ class PasswordWidget extends StatefulWidget { this.hintText, this.errorText, this.title, + this.maxLength, }) : super(key: key); final TextEditingController controller; @@ -688,6 +693,7 @@ class PasswordWidget extends StatefulWidget { final String? hintText; final String? errorText; final String? title; + final int? maxLength; @override State createState() => _PasswordWidgetState(); @@ -750,6 +756,7 @@ class _PasswordWidgetState extends State { obscureText: !_passwordVisible, errorText: widget.errorText, focusNode: _focusNode, + maxLength: widget.maxLength, ); } } @@ -1123,7 +1130,7 @@ void showRequestElevationDialog( errorText: errPwd.isEmpty ? null : errPwd.value, ), ], - ).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), + ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0), ).marginOnly(top: 10), ], ), @@ -2244,6 +2251,7 @@ void changeUnlockPinDialog(String oldPin, Function() callback) { final confirmController = TextEditingController(text: oldPin); String? pinErrorText; String? confirmationErrorText; + final maxLength = bind.mainMaxEncryptLen(); gFFI.dialogManager.show((setState, close, context) { submit() async { pinErrorText = null; @@ -2277,12 +2285,14 @@ void changeUnlockPinDialog(String oldPin, Function() callback) { controller: pinController, obscureText: true, errorText: pinErrorText, + maxLength: maxLength, ), DialogTextField( title: translate('Confirmation'), controller: confirmController, obscureText: true, errorText: confirmationErrorText, + maxLength: maxLength, ) ], ).marginOnly(bottom: 12), diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 0d9cc007ccc..867d71dff2d 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -45,15 +46,15 @@ class _MyGroupState extends State { retry: null, close: () => gFFI.groupModel.groupLoadError.value = ''), Expanded( - child: (isDesktop || isWebDesktop) - ? _buildDesktop() - : _buildMobile()) + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildPortrait() + : _buildLandscape())), ], ); }); } - Widget _buildDesktop() { + Widget _buildLandscape() { return Row( children: [ Container( @@ -82,14 +83,14 @@ class _MyGroupState extends State { child: Align( alignment: Alignment.topLeft, child: MyGroupPeerView( - menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.groupModel.peers)), + menuPadding: widget.menuPadding, + )), ) ], ); } - Widget _buildMobile() { + Widget _buildPortrait() { return Column( children: [ Container( @@ -114,8 +115,8 @@ class _MyGroupState extends State { child: Align( alignment: Alignment.topLeft, child: MyGroupPeerView( - menuPadding: widget.menuPadding, - getInitPeers: () => gFFI.groupModel.peers)), + menuPadding: widget.menuPadding, + )), ) ], ); @@ -159,14 +160,14 @@ class _MyGroupState extends State { } return true; }).toList(); - final listView = ListView.builder( - shrinkWrap: isMobile, + listView(bool isPortrait) => ListView.builder( + shrinkWrap: isPortrait, itemCount: items.length, itemBuilder: (context, index) => _buildUserItem(items[index])); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); - return (isDesktop || isWebDesktop) - ? listView - : LimitedBox(maxHeight: maxHeight, child: listView); + return Obx(() => stateGlobal.isPortrait.isFalse + ? listView(false) + : LimitedBox(maxHeight: maxHeight, child: listView(true))); }); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f9dd73bbf6b..0a15eb45b88 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -53,42 +54,44 @@ class _PeerCardState extends State<_PeerCard> @override Widget build(BuildContext context) { super.build(context); - if (isDesktop || isWebDesktop) { - return _buildDesktop(); - } else { - return _buildMobile(); - } + return Obx(() => + stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape()); } - Widget _buildMobile() { - final peer = super.widget.peer; + Widget gestureDetector({required Widget child}) { final PeerTabModel peerTabModel = Provider.of(context); - return Card( - margin: EdgeInsets.symmetric(horizontal: 2), - child: GestureDetector( - onTap: () { - if (peerTabModel.multiSelectionMode) { - peerTabModel.select(peer); + final peer = super.widget.peer; + return GestureDetector( + onDoubleTap: peerTabModel.multiSelectionMode + ? null + : () => widget.connect(context, peer.id), + onTap: () { + if (peerTabModel.multiSelectionMode) { + peerTabModel.select(peer); + } else { + if (isMobile) { + widget.connect(context, peer.id); } else { - if (!isWebDesktop) { - connectInPeerTab(context, peer, widget.tab); - } + peerTabModel.select(peer); } - }, - onDoubleTap: isWebDesktop - ? () => connectInPeerTab(context, peer, widget.tab) - : null, - onLongPress: () { - peerTabModel.select(peer); - }, + } + }, + onLongPress: () => peerTabModel.select(peer), + child: child); + } + + Widget _buildPortrait() { + final peer = super.widget.peer; + return Card( + margin: EdgeInsets.symmetric(horizontal: 2), + child: gestureDetector( child: Container( padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), child: _buildPeerTile(context, peer, null)), )); } - Widget _buildDesktop() { - final PeerTabModel peerTabModel = Provider.of(context); + Widget _buildLandscape() { final peer = super.widget.peer; var deco = Rx( BoxDecoration( @@ -117,36 +120,27 @@ class _PeerCardState extends State<_PeerCard> ), ); }, - child: GestureDetector( - onDoubleTap: - peerTabModel.multiSelectionMode || peerTabModel.isShiftDown - ? null - : () => widget.connect(context, peer.id), - onTap: () => peerTabModel.select(peer), - onLongPress: () => peerTabModel.select(peer), + child: gestureDetector( child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), ); } - Widget _buildPeerTile( - BuildContext context, Peer peer, Rx? deco) { - hideUsernameOnCard ??= - bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; + makeChild(bool isPortrait, Peer peer) { final name = hideUsernameOnCard == true ? peer.hostname : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); - final child = Row( + return Row( mainAxisSize: MainAxisSize.max, children: [ Container( decoration: BoxDecoration( color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: isMobile + borderRadius: isPortrait ? BorderRadius.circular(_tileRadius) : BorderRadius.only( topLeft: Radius.circular(_tileRadius), @@ -154,11 +148,11 @@ class _PeerCardState extends State<_PeerCard> ), ), alignment: Alignment.center, - width: isMobile ? 50 : 42, - height: isMobile ? 50 : null, + width: isPortrait ? 50 : 42, + height: isPortrait ? 50 : null, child: Stack( children: [ - getPlatformImage(peer.platform, size: isMobile ? 38 : 30) + getPlatformImage(peer.platform, size: isPortrait ? 38 : 30) .paddingAll(6), if (_shouldBuildPasswordIcon(peer)) Positioned( @@ -183,19 +177,19 @@ class _PeerCardState extends State<_PeerCard> child: Column( children: [ Row(children: [ - getOnline(isMobile ? 4 : 8, peer.online), + getOnline(isPortrait ? 4 : 8, peer.online), Expanded( child: Text( peer.alias.isEmpty ? formatID(peer.id) : peer.alias, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, )), - ]).marginOnly(top: isMobile ? 0 : 2), + ]).marginOnly(top: isPortrait ? 0 : 2), Align( alignment: Alignment.centerLeft, child: Text( name, - style: isMobile ? null : greyStyle, + style: isPortrait ? null : greyStyle, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, ), @@ -203,41 +197,47 @@ class _PeerCardState extends State<_PeerCard> ], ).marginOnly(top: 2), ), - isMobile - ? checkBoxOrActionMoreMobile(peer) - : checkBoxOrActionMoreDesktop(peer, isTile: true), + isPortrait + ? checkBoxOrActionMorePortrait(peer) + : checkBoxOrActionMoreLandscape(peer, isTile: true), ], ).paddingOnly(left: 10.0, top: 3.0), ), ) ], ); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx? deco) { + hideUsernameOnCard ??= + bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; final colors = _frontN(peer.tags, 25) .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( - message: isMobile + message: !(isDesktop || isWebDesktop) ? '' : peer.tags.isNotEmpty ? '${translate('Tags')}: ${peer.tags.join(', ')}' : '', child: Stack(children: [ - deco == null - ? child - : Obx( - () => Container( + Obx( + () => deco == null + ? makeChild(stateGlobal.isPortrait.isTrue, peer) + : Container( foregroundDecoration: deco.value, - child: child, + child: makeChild(stateGlobal.isPortrait.isTrue, peer), ), - ), + ), if (colors.isNotEmpty) - Positioned( - top: 2, - right: isMobile ? 20 : 10, - child: CustomPaint( - painter: TagPainter(radius: 3, colors: colors), - ), - ) + Obx(() => Positioned( + top: 2, + right: stateGlobal.isPortrait.isTrue ? 20 : 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + )) ]), ); } @@ -253,6 +253,9 @@ class _PeerCardState extends State<_PeerCard> color: Colors.transparent, elevation: 0, margin: EdgeInsets.zero, + // to-do: memory leak here, more investigation needed. + // Continious rebuilds of `Obx()` will cause memory leak here. + // The simple demo does not have this issue. child: Obx( () => Container( foregroundDecoration: deco.value, @@ -316,7 +319,7 @@ class _PeerCardState extends State<_PeerCard> style: Theme.of(context).textTheme.titleSmall, )), ]).paddingSymmetric(vertical: 8)), - checkBoxOrActionMoreDesktop(peer, isTile: false), + checkBoxOrActionMoreLandscape(peer, isTile: false), ], ).paddingSymmetric(horizontal: 12.0), ) @@ -362,7 +365,7 @@ class _PeerCardState extends State<_PeerCard> } } - Widget checkBoxOrActionMoreMobile(Peer peer) { + Widget checkBoxOrActionMorePortrait(Peer peer) { final PeerTabModel peerTabModel = Provider.of(context); final selected = peerTabModel.isPeerSelected(peer.id); if (peerTabModel.multiSelectionMode) { @@ -390,7 +393,7 @@ class _PeerCardState extends State<_PeerCard> } } - Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) { + Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) { final PeerTabModel peerTabModel = Provider.of(context); final selected = peerTabModel.isPeerSelected(peer.id); if (peerTabModel.multiSelectionMode) { @@ -876,7 +879,7 @@ class RecentPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -935,7 +938,7 @@ class FavoritePeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -988,7 +991,7 @@ class DiscoveredPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -1041,7 +1044,7 @@ class AddressBookPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1173,7 +1176,7 @@ class MyGroupPeerCard extends BasePeerCard { BuildContext context) async { final List> menuItems = [ _connectAction(context), - if (!isWeb) _transferFileAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1203,6 +1206,7 @@ class MyGroupPeerCard extends BasePeerCard { } void _rdpDialog(String id) async { + final maxLength = bind.mainMaxEncryptLen(); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final portController = TextEditingController(text: port); @@ -1257,54 +1261,54 @@ void _rdpDialog(String id) async { ), ], ).marginOnly(bottom: isDesktop ? 8 : 0), - Row( - children: [ - (isDesktop || isWebDesktop) - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - "${translate('Username')}:", - textAlign: TextAlign.right, - ).marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: TextField( - decoration: InputDecoration( - labelText: (isDesktop || isWebDesktop) - ? null - : translate('Username')), - controller: userController, - ), - ), - ], - ).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0), - Row( - children: [ - (isDesktop || isWebDesktop) - ? ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - "${translate('Password')}:", - textAlign: TextAlign.right, - ).marginOnly(right: 10)) - : SizedBox.shrink(), - Expanded( - child: Obx(() => TextField( - obscureText: secure.value, + Obx(() => Row( + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: TextField( decoration: InputDecoration( - labelText: (isDesktop || isWebDesktop) - ? null - : translate('Password'), - suffixIcon: IconButton( - onPressed: () => secure.value = !secure.value, - icon: Icon(secure.value - ? Icons.visibility_off - : Icons.visibility))), - controller: passwordController, - )), - ), - ], - ) + labelText: + isDesktop ? null : translate('Username')), + controller: userController, + ), + ), + ], + ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)), + Obx(() => Row( + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: Obx(() => TextField( + obscureText: secure.value, + maxLength: maxLength, + decoration: InputDecoration( + labelText: + isDesktop ? null : translate('Password'), + suffixIcon: IconButton( + onPressed: () => + secure.value = !secure.value, + icon: Icon(secure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: passwordController, + )), + ), + ], + )) ], ), ), diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 8fe73144999..35975078805 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -16,6 +16,7 @@ import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -107,33 +108,33 @@ class _PeerTabPageState extends State Widget build(BuildContext context) { final model = Provider.of(context); Widget selectionWrap(Widget widget) { - return model.multiSelectionMode ? createMultiSelectionBar() : widget; + return model.multiSelectionMode ? createMultiSelectionBar(model) : widget; } return Column( textBaseline: TextBaseline.ideographic, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 32, - child: Container( - padding: (isDesktop || isWebDesktop) - ? null - : EdgeInsets.symmetric(horizontal: 2), - child: selectionWrap(Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: - visibleContextMenuListener(_createSwitchBar(context))), - if (isMobile) - ..._mobileRightActions(context) - else - ..._desktopRightActions(context) - ], - )), - ), - ).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), + Obx(() => SizedBox( + height: 32, + child: Container( + padding: stateGlobal.isPortrait.isTrue + ? EdgeInsets.symmetric(horizontal: 2) + : null, + child: selectionWrap(Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), + if (stateGlobal.isPortrait.isTrue) + ..._portraitRightActions(context) + else + ..._landscapeRightActions(context) + ], + )), + ), + ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)), _createPeersView(), ], ); @@ -299,7 +300,7 @@ class _PeerTabPageState extends State } Widget visibleContextMenuListener(Widget child) { - if (isMobile) { + if (!(isDesktop || isWebDesktop)) { return GestureDetector( onLongPressDown: (e) { final x = e.globalPosition.dx; @@ -361,8 +362,7 @@ class _PeerTabPageState extends State .toList()); } - Widget createMultiSelectionBar() { - final model = Provider.of(context); + Widget createMultiSelectionBar(PeerTabModel model) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -380,7 +380,7 @@ class _PeerTabPageState extends State Row( children: [ selectionCount(model.selectedPeers.length), - selectAll(), + selectAll(model), closeSelection(), ], ) @@ -456,7 +456,7 @@ class _PeerTabPageState extends State showToast(translate('Successful')); }, child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), - ).marginOnly(left: isMobile ? 11 : 6), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -477,7 +477,7 @@ class _PeerTabPageState extends State model.setMultiSelectionMode(false); }, child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), - ).marginOnly(left: isMobile ? 11 : 6), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -500,7 +500,7 @@ class _PeerTabPageState extends State }); }, child: Icon(Icons.tag)) - .marginOnly(left: isMobile ? 11 : 6), + .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), ); } @@ -511,8 +511,7 @@ class _PeerTabPageState extends State ); } - Widget selectAll() { - final model = Provider.of(context); + Widget selectAll(PeerTabModel model) { return Offstage( offstage: model.selectedPeers.length >= model.currentTabCachedPeers.length, @@ -556,10 +555,10 @@ class _PeerTabPageState extends State }); } - List _desktopRightActions(BuildContext context) { + List _landscapeRightActions(BuildContext context) { final model = Provider.of(context); return [ - const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), + const PeerSearchBar().marginOnly(right: 13), _createRefresh( index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), _createRefresh( @@ -580,7 +579,7 @@ class _PeerTabPageState extends State ]; } - List _mobileRightActions(BuildContext context) { + List _portraitRightActions(BuildContext context) { final model = Provider.of(context); final screenWidth = MediaQuery.of(context).size.width; final leftIconSize = Theme.of(context).iconTheme.size ?? 24; @@ -701,13 +700,13 @@ class _PeerSearchBarState extends State { baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); }); - return Container( - width: isMobile ? 120 : 140, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(6), - ), - child: Obx(() => Row( + return Obx(() => Container( + width: stateGlobal.isPortrait.isTrue ? 120 : 140, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ), + child: Row( children: [ Expanded( child: Row( @@ -768,8 +767,8 @@ class _PeerSearchBarState extends State { ), ) ], - )), - ); + ), + )); } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index ef9647eaa93..7f16850219f 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -41,6 +43,14 @@ class LoadEvent { static const String group = 'load_group_peers'; } +class PeersModelName { + static const String recent = 'recent peer'; + static const String favorite = 'fav peer'; + static const String lan = 'discovered peer'; + static const String addressBook = 'address book peer'; + static const String group = 'group peer'; +} + /// for peer search text, global obs value final peerSearchText = "".obs; @@ -88,6 +98,7 @@ class _PeersViewState extends State<_PeersView> var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; var _lastQueryTime = DateTime.now(); + var _lastWindowRestoreTime = DateTime.now(); var _queryCount = 0; var _exit = false; bool _isActive = true; @@ -116,11 +127,38 @@ class _PeersViewState extends State<_PeersView> @override void onWindowFocus() { _queryCount = 0; + _isActive = true; } @override - void onWindowMinimize() { + void onWindowBlur() { + // We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`. + // Maybe it's a bug of the window manager, but the source code seems to be correct. + // + // Although `onWindowRestore()` is called after `onWindowBlur()` in my test, + // we need the following comparison to ensure that `_isActive` is true in the end. + if (isWindows && + DateTime.now().difference(_lastWindowRestoreTime) < + const Duration(milliseconds: 300)) { + return; + } _queryCount = _maxQueryCount; + _isActive = false; + } + + @override + void onWindowRestore() { + // Window restore (on MacOS and Linux) also triggers `onWindowFocus()`. + // But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager. + if (!isWindows) return; + _queryCount = 0; + _isActive = true; + _lastWindowRestoreTime = DateTime.now(); + } + + @override + void onWindowMinimize() { + // Window minimize also triggers `onWindowBlur()`. } // This function is required for mobile. @@ -128,7 +166,7 @@ class _PeersViewState extends State<_PeersView> @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - if (isDesktop) return; + if (isDesktop || isWebDesktop) return; if (state == AppLifecycleState.resumed) { _isActive = true; _queryCount = 0; @@ -139,8 +177,11 @@ class _PeersViewState extends State<_PeersView> @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => widget.peers, + // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. + // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. + // Simple demo can reproduce this issue. + return ChangeNotifierProvider.value( + value: widget.peers, child: Consumer(builder: (context, peers, child) { if (peers.peers.isEmpty) { gFFI.peerTabModel.setCurrentTabCachedPeers([]); @@ -194,7 +235,7 @@ class _PeersViewState extends State<_PeersView> var peers = snapshot.data!; if (peers.length > 1000) peers = peers.sublist(0, 1000); gFFI.peerTabModel.setCurrentTabCachedPeers(peers); - buildOnePeer(Peer peer) { + buildOnePeer(Peer peer, bool isPortrait) { final visibilityChild = VisibilityDetector( key: ValueKey(_cardId(peer.id)), onVisibilityChanged: onVisibilityChanged, @@ -206,7 +247,7 @@ class _PeersViewState extends State<_PeersView> // No need to listen the currentTab change event. // Because the currentTab change event will trigger the peers change event, // and the peers change event will trigger _buildPeersView(). - return (isDesktop || isWebDesktop) + return !isPortrait ? Obx(() => peerCardUiType.value == PeerUiType.list ? Container(height: 45, child: visibilityChild) : peerCardUiType.value == PeerUiType.grid @@ -217,44 +258,45 @@ class _PeersViewState extends State<_PeersView> : Container(child: visibilityChild); } - final Widget child; - if (isMobile) { - child = ListView.builder( - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]).marginOnly( - top: index == 0 ? 0 : space / 2, bottom: space / 2); - }, - ); - } else { - child = Obx(() => peerCardUiType.value == PeerUiType.list - ? DesktopScrollWrapper( - scrollController: _scrollController, - child: ListView.builder( - controller: _scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]).marginOnly( - right: space, - top: index == 0 ? 0 : space / 2, - bottom: space / 2); - }), - ) - : DesktopScrollWrapper( - scrollController: _scrollController, - child: DynamicGridView.builder( - controller: _scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithWrapping( - mainAxisSpacing: space / 2, - crossAxisSpacing: space), - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index]); - }), - )); - } + // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6. + // Continious rebuilds of `ListView.builder` will cause memory leak. + // Simple demo can reproduce this issue. + final Widget child = Obx(() => stateGlobal.isPortrait.isTrue + ? ListView.builder( + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], true).marginOnly( + top: index == 0 ? 0 : space / 2, bottom: space / 2); + }, + ) + : peerCardUiType.value == PeerUiType.list + ? DesktopScrollWrapper( + scrollController: _scrollController, + child: ListView.builder( + controller: _scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false) + .marginOnly( + right: space, + top: index == 0 ? 0 : space / 2, + bottom: space / 2); + }), + ) + : DesktopScrollWrapper( + scrollController: _scrollController, + child: DynamicGridView.builder( + controller: _scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithWrapping( + mainAxisSpacing: space / 2, + crossAxisSpacing: space), + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false); + }), + )); if (updateEvent == UpdateEvent.load) { _curPeers.clear(); @@ -290,7 +332,12 @@ class _PeersViewState extends State<_PeersView> _queryOnlines(false); } } else { - if (_isActive && (_queryCount < _maxQueryCount || !p)) { + final skipIfIsWeb = + isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage); + final skipIfMobile = + (isAndroid || isIOS) && !stateGlobal.isInMainPage; + final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive; + if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) { if (now.difference(_lastQueryTime) >= _queryInterval) { if (_curPeers.isNotEmpty) { bind.queryOnlines(ids: _curPeers.toList(growable: false)); @@ -371,28 +418,39 @@ class _PeersViewState extends State<_PeersView> } abstract class BasePeersView extends StatelessWidget { - final String name; - final String loadEvent; + final PeerTabIndex peerTabIndex; final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; - final GetInitPeers? getInitPeers; const BasePeersView({ Key? key, - required this.name, - required this.loadEvent, + required this.peerTabIndex, this.peerFilter, required this.peerCardBuilder, - required this.getInitPeers, }) : super(key: key); @override Widget build(BuildContext context) { + Peers peers; + switch (peerTabIndex) { + case PeerTabIndex.recent: + peers = gFFI.recentPeersModel; + break; + case PeerTabIndex.fav: + peers = gFFI.favoritePeersModel; + break; + case PeerTabIndex.lan: + peers = gFFI.lanPeersModel; + break; + case PeerTabIndex.ab: + peers = gFFI.abModel.peersModel; + break; + case PeerTabIndex.group: + peers = gFFI.groupModel.peersModel; + break; + } return _PeersView( - peers: - Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers), - peerFilter: peerFilter, - peerCardBuilder: peerCardBuilder); + peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder); } } @@ -401,13 +459,11 @@ class RecentPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'recent peer', - loadEvent: LoadEvent.recent, + peerTabIndex: PeerTabIndex.recent, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -423,13 +479,11 @@ class FavoritePeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'favorite peer', - loadEvent: LoadEvent.favorite, + peerTabIndex: PeerTabIndex.fav, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -445,13 +499,11 @@ class DiscoveredPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'discovered peer', - loadEvent: LoadEvent.lan, + peerTabIndex: PeerTabIndex.lan, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: null, ); @override @@ -464,21 +516,16 @@ class DiscoveredPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView { AddressBookPeersView( - {Key? key, - EdgeInsets? menuPadding, - ScrollController? scrollController, - required GetInitPeers getInitPeers}) + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'address book peer', - loadEvent: LoadEvent.addressBook, + peerTabIndex: PeerTabIndex.ab, peerFilter: (Peer peer) => _hitTag(gFFI.abModel.selectedTags, peer.tags), peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: getInitPeers, ); static bool _hitTag(List selectedTags, List idents) { @@ -505,20 +552,15 @@ class AddressBookPeersView extends BasePeersView { class MyGroupPeerView extends BasePeersView { MyGroupPeerView( - {Key? key, - EdgeInsets? menuPadding, - ScrollController? scrollController, - required GetInitPeers getInitPeers}) + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) : super( key: key, - name: 'group peer', - loadEvent: LoadEvent.group, + peerTabIndex: PeerTabIndex.group, peerFilter: filter, peerCardBuilder: (Peer peer) => MyGroupPeerCard( peer: peer, menuPadding: menuPadding, ), - getInitPeers: getInitPeers, ); static bool filter(Peer peer) { diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 61bd4dd31bc..a4d3caf2990 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { + // https://github.com/flutter/flutter/issues/154053 + final useRawKeyEvents = isLinux && !isWeb; + // FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events, + // while `Alt` and `Control` are seperated key events for en-US input method. return FocusScope( autofocus: true, child: Focus( @@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: (FocusNode data, RawKeyEvent e) => - inputModel.handleRawKeyEvent(e), + onKey: useRawKeyEvents + ? (FocusNode data, RawKeyEvent event) => + inputModel.handleRawKeyEvent(event) + : null, + onKeyEvent: useRawKeyEvents + ? null + : (FocusNode node, KeyEvent event) => + inputModel.handleKeyEvent(event), child: child)); } } @@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { return; } - if (isDesktop) { + if (isDesktop || isWebDesktop) { ffi.cursorModel.trySetRemoteWindowCoords(); } // Workaround for the issue that the first pan event is sent a long time after the start event. @@ -275,7 +285,7 @@ class _RawTouchGestureDetectorRegionState if (lastDeviceKind != PointerDeviceKind.touch) { return; } - if (isDesktop) { + if (isDesktop || isWebDesktop) { ffi.cursorModel.clearRemoteWindowCoords(); } inputModel.sendMouse('up', MouseButtons.left); diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 0b56d9f4c14..153121057e5 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -147,12 +147,23 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } + + connectWithToken( + {required bool isFileTransfer, required bool isTcpTunneling}) { + final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); + connect(context, id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + connToken: connToken); + } + // transferFile if (isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => connect(context, id, isFileTransfer: true)), + onPressed: () => + connectWithToken(isFileTransfer: true, isTcpTunneling: false)), ); } // tcpTunneling @@ -160,7 +171,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => connect(context, id, isTcpTunneling: true)), + onPressed: () => + connectWithToken(isFileTransfer: false, isTcpTunneling: true)), ); } // note @@ -183,7 +195,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { v.add( TTextMenu( - child: Text('${translate("Insert")} Ctrl + Alt + Del'), + child: Text('${translate("Insert Ctrl + Alt + Del")}'), onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), ); } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a5414dd0db0..89306bb7ae8 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformAndroid = "Android"; +const String kPeerPlatformWebDesktop = "WebDesktop"; const double kScrollbarThickness = 12.0; @@ -88,6 +89,7 @@ const String kOptionAllowAutoDisconnect = "allow-auto-disconnect"; const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout"; const String kOptionEnableHwcodec = "enable-hwcodec"; const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming"; +const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing"; const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionAccessMode = "access-mode"; const String kOptionEnableKeyboard = "enable-keyboard"; @@ -200,7 +202,7 @@ const double kMinFps = 5; const double kDefaultFps = 30; const double kMaxFps = 120; -const double kMinQuality = 10; +const double kMinQuality = 5; const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; @@ -569,3 +571,5 @@ enum WindowsTarget { extension WindowsTargetExt on int { WindowsTarget get windowsVersion => getWindowsTarget(this); } + +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 1f917d2d19e..bc3c123de95 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; @@ -323,36 +323,7 @@ class _ConnectionPageState extends State child: Ink( child: Column( children: [ - Row( - children: [ - Expanded( - child: Row( - children: [ - AutoSizeText( - translate('Control Remote Desktop'), - maxLines: 1, - style: Theme.of(context) - .textTheme - .titleLarge - ?.merge(TextStyle(height: 1)), - ).marginOnly(right: 4), - Tooltip( - waitDuration: Duration(milliseconds: 300), - message: translate("id_input_tip"), - child: Icon( - Icons.help_outline_outlined, - size: 16, - color: Theme.of(context) - .textTheme - .titleLarge - ?.color - ?.withOpacity(0.5), - ), - ), - ], - )), - ], - ).marginOnly(bottom: 15), + getConnectionPageTitle(context, false).marginOnly(bottom: 15), Row( children: [ Expanded( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 31a8e1374ff..493e4ca47bb 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -664,9 +664,17 @@ class _DesktopHomePageState extends State void initState() { super.initState(); if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + setState(() { + updateUrl = evt['url']; + }); + } + }); Timer(const Duration(seconds: 1), () async { - updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (updateUrl.isNotEmpty) setState(() {}); + bind.mainGetSoftwareUpdateUrl(); }); } _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { @@ -766,6 +774,7 @@ class _DesktopHomePageState extends State isRDP: call.arguments['isRDP'], password: call.arguments['password'], forceRelay: call.arguments['forceRelay'], + connToken: call.arguments['connToken'], ); } else if (call.method == kWindowEventMoveTabToNewWindow) { final args = call.arguments.split(','); @@ -824,6 +833,10 @@ class _DesktopHomePageState extends State _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); + if (!bind.isCustomClient()) { + platformFFI.unregisterEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); + } super.dispose(); } @@ -857,6 +870,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { // SpecialCharacterValidationRule(), MinCharactersValidationRule(8), ]; + final maxLength = bind.mainMaxEncryptLen(); gFFI.dialogManager.show((setState, close, context) { submit() { @@ -915,6 +929,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { errMsg0 = ''; }); }, + maxLength: maxLength, ), ), ], @@ -941,6 +956,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { errMsg1 = ''; }); }, + maxLength: maxLength, ), ), ], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 77f94308bee..23e44579a47 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget { final SettingsTabKey initialTabkey; static final List tabKeys = [ SettingsTabKey.general, - if (!bind.isOutgoingOnly() && + if (!isWeb && + !bind.isOutgoingOnly() && !bind.isDisableSettings() && bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') SettingsTabKey.safety, @@ -216,7 +217,7 @@ class _DesktopSettingPageState extends State width: _kTabWidth, child: Column( children: [ - _header(), + _header(context), Flexible(child: _listView(tabs: _settingTabs())), ], ), @@ -239,21 +240,40 @@ class _DesktopSettingPageState extends State ); } - Widget _header() { + Widget _header(BuildContext context) { + final settingsText = Text( + translate('Settings'), + textAlign: TextAlign.left, + style: const TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ); return Row( children: [ - SizedBox( - height: 62, - child: Text( - translate('Settings'), - textAlign: TextAlign.left, - style: const TextStyle( - color: _accentColor, - fontSize: _kTitleFontSize, - fontWeight: FontWeight.w400, + if (isWeb) + IconButton( + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + icon: Icon(Icons.arrow_back), + ).marginOnly(left: 5), + if (isWeb) + SizedBox( + height: 62, + child: Align( + alignment: Alignment.center, + child: settingsText, ), - ), - ).marginOnly(left: 20, top: 10), + ).marginOnly(left: 20), + if (!isWeb) + SizedBox( + height: 62, + child: settingsText, + ).marginOnly(left: 20, top: 10), const Spacer(), ], ); @@ -322,7 +342,8 @@ class _General extends StatefulWidget { } class _GeneralState extends State<_General> { - final RxBool serviceStop = Get.find(tag: 'stop-service'); + final RxBool serviceStop = + isWeb ? RxBool(false) : Get.find(tag: 'stop-service'); RxBool serviceBtnEnabled = true.obs; @override @@ -334,13 +355,13 @@ class _GeneralState extends State<_General> { physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ - service(), + if (!isWeb) service(), theme(), _Card(title: 'Language', children: [language()]), - hwcodec(), - audio(context), - record(context), - WaylandCard(), + if (!isWeb) hwcodec(), + if (!isWeb) audio(context), + if (!isWeb) record(context), + if (!isWeb) WaylandCard(), other() ], ).marginOnly(bottom: _kListViewBottomMargin)); @@ -348,8 +369,8 @@ class _GeneralState extends State<_General> { Widget theme() { final current = MyTheme.getThemeModePreference().toShortString(); - onChanged(String value) { - MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); + onChanged(String value) async { + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); setState(() {}); } @@ -394,13 +415,13 @@ class _GeneralState extends State<_General> { Widget other() { final children = [ - if (!bind.isIncomingOnly()) + if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), - wallpaper(), - if (!bind.isIncomingOnly()) ...[ + if (!isWeb) wallpaper(), + if (!isWeb && !bind.isIncomingOnly()) ...[ _OptionCheckBox( context, 'Open connection in new tab', @@ -417,18 +438,19 @@ class _GeneralState extends State<_General> { kOptionAllowAlwaysSoftwareRender, ), ), - Tooltip( - message: translate('texture_render_tip'), - child: _OptionCheckBox( - context, - "Use texture rendering", - kOptionTextureRender, - optGetter: bind.mainGetUseTextureRender, - optSetter: (k, v) async => - await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + if (!isWeb) + Tooltip( + message: translate('texture_render_tip'), + child: _OptionCheckBox( + context, + "Use texture rendering", + kOptionTextureRender, + optGetter: bind.mainGetUseTextureRender, + optSetter: (k, v) async => + await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + ), ), - ), - if (!bind.isCustomClient()) + if (!isWeb && !bind.isCustomClient()) _OptionCheckBox( context, 'Check for software update on startup', @@ -443,7 +465,7 @@ class _GeneralState extends State<_General> { ) ], ]; - if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { + if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { children.add(_OptionCheckBox( context, 'Allow linux headless', kOptionAllowLinuxHeadless)); } @@ -553,12 +575,18 @@ class _GeneralState extends State<_General> { bool root_dir_exists = map['root_dir_exists']!; bool user_dir_exists = map['user_dir_exists']!; return _Card(title: 'Recording', children: [ - _OptionCheckBox(context, 'Automatically record incoming sessions', - kOptionAllowAutoRecordIncoming), - if (showRootDir) + if (!bind.isOutgoingOnly()) + _OptionCheckBox(context, 'Automatically record incoming sessions', + kOptionAllowAutoRecordIncoming), + if (!bind.isIncomingOnly()) + _OptionCheckBox(context, 'Automatically record outgoing sessions', + kOptionAllowAutoRecordOutgoing, + isServer: false), + if (showRootDir && !bind.isOutgoingOnly()) Row( children: [ - Text('${translate("Incoming")}:'), + Text( + '${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'), Expanded( child: GestureDetector( onTap: root_dir_exists @@ -575,45 +603,49 @@ class _GeneralState extends State<_General> { ), ], ).marginOnly(left: _kContentHMargin), - Row( - children: [ - Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'), - Expanded( - child: GestureDetector( - onTap: user_dir_exists - ? () => launchUrl(Uri.file(user_dir)) - : null, - child: Text( - user_dir, - softWrap: true, - style: user_dir_exists - ? const TextStyle(decoration: TextDecoration.underline) + if (!(showRootDir && bind.isIncomingOnly())) + Row( + children: [ + Text( + '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'), + Expanded( + child: GestureDetector( + onTap: user_dir_exists + ? () => launchUrl(Uri.file(user_dir)) : null, - )).marginOnly(left: 10), - ), - ElevatedButton( - onPressed: isOptionFixed(kOptionVideoSaveDirectory) - ? null - : () async { - String? initialDirectory; - if (await Directory.fromUri(Uri.directory(user_dir)) - .exists()) { - initialDirectory = user_dir; - } - String? selectedDirectory = - await FilePicker.platform.getDirectoryPath( - initialDirectory: initialDirectory); - if (selectedDirectory != null) { - await bind.mainSetOption( - key: kOptionVideoSaveDirectory, - value: selectedDirectory); - setState(() {}); - } - }, - child: Text(translate('Change'))) - .marginOnly(left: 5), - ], - ).marginOnly(left: _kContentHMargin), + child: Text( + user_dir, + softWrap: true, + style: user_dir_exists + ? const TextStyle( + decoration: TextDecoration.underline) + : null, + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: isOptionFixed(kOptionVideoSaveDirectory) + ? null + : () async { + String? initialDirectory; + if (await Directory.fromUri( + Uri.directory(user_dir)) + .exists()) { + initialDirectory = user_dir; + } + String? selectedDirectory = + await FilePicker.platform.getDirectoryPath( + initialDirectory: initialDirectory); + if (selectedDirectory != null) { + await bind.mainSetLocalOption( + key: kOptionVideoSaveDirectory, + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), ]); }); } @@ -641,8 +673,9 @@ class _GeneralState extends State<_General> { initialKey: currentKey, onChanged: (key) async { await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); - reloadAllWindows(); - bind.mainChangeLanguage(lang: key); + if (isWeb) reloadCurrentWindow(); + if (!isWeb) reloadAllWindows(); + if (!isWeb) bind.mainChangeLanguage(lang: key); }, enabled: !isOptFixed, ).marginOnly(left: _kContentHMargin); @@ -1337,7 +1370,7 @@ class _Network extends StatefulWidget { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; - bool locked = bind.mainIsInstalled(); + bool locked = !isWeb && bind.mainIsInstalled(); @override Widget build(BuildContext context) { @@ -1346,8 +1379,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { final scrollController = ScrollController(); final hideServer = bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; + // TODO: support web proxy final hideProxy = - bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; + isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; return DesktopScrollWrapper( scrollController: scrollController, child: ListView( @@ -1427,8 +1461,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { children: [ Obx(() => _LabeledTextField(context, 'ID Server', idController, idErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'Relay Server', - relayController, relayErrMsg.value, enabled, secure)), + if (!isWeb) + Obx(() => _LabeledTextField(context, 'Relay Server', + relayController, relayErrMsg.value, enabled, secure)), Obx(() => _LabeledTextField(context, 'API Server', apiController, apiErrMsg.value, enabled, secure)), _LabeledTextField( @@ -1467,7 +1502,7 @@ class _DisplayState extends State<_Display> { scrollStyle(context), imageQuality(context), codec(context), - privacyModeImpl(context), + if (!isWeb) privacyModeImpl(context), other(context), ]).marginOnly(bottom: _kListViewBottomMargin)); } @@ -1878,9 +1913,10 @@ class _AboutState extends State<_About> { SelectionArea( child: Text('${translate('Build Date')}: $buildDate') .marginSymmetric(vertical: 4.0)), - SelectionArea( - child: Text('${translate('Fingerprint')}: $fingerprint') - .marginSymmetric(vertical: 4.0)), + if (!isWeb) + SelectionArea( + child: Text('${translate('Fingerprint')}: $fingerprint') + .marginSymmetric(vertical: 4.0)), InkWell( onTap: () { 'RustDesk ${launchUrlString('https://rustdesk.com/privacy.html')}'; @@ -2495,6 +2531,7 @@ void changeSocks5Proxy() async { : Icons.visibility))), controller: pwdController, enabled: !isOptFixed, + maxLength: bind.mainMaxEncryptLen(), )), ), ], diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index c9e565fd776..90b8d7dcbf3 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:extended_text/extended_text.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -16,6 +17,8 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:flutter_hbb/web/dummy.dart' + if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; @@ -54,21 +57,23 @@ class FileManagerPage extends StatefulWidget { required this.id, required this.password, required this.isSharedPassword, - required this.tabController, + this.tabController, + this.connToken, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; final bool? forceRelay; - final DesktopTabController tabController; + final String? connToken; + final DesktopTabController? tabController; @override State createState() => _FileManagerPageState(); } class _FileManagerPageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final _mouseFocusScope = Rx(MouseFocusScope.none); final _dropMaskVisible = false.obs; // TODO impl drop mask @@ -87,6 +92,7 @@ class _FileManagerPageState extends State isFileTransfer: true, password: widget.password, isSharedPassword: widget.isSharedPassword, + connToken: widget.connToken, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager @@ -96,12 +102,16 @@ class _FileManagerPageState extends State if (!isLinux) { WakelockPlus.enable(); } + if (isWeb) { + _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); + } debugPrint("File manager page init success with id ${widget.id}"); _ffi.dialogManager.setOverlayState(_overlayKeyState); // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. WidgetsBinding.instance.addPostFrameCallback((_) { - widget.tabController.onSelected?.call(widget.id); + widget.tabController?.onSelected?.call(widget.id); }); + WidgetsBinding.instance.addObserver(this); } @override @@ -114,12 +124,21 @@ class _FileManagerPageState extends State } Get.delete(tag: 'ft_${widget.id}'); }); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override bool get wantKeepAlive => true; + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + jobController.jobTable.refresh(); + } + } + @override Widget build(BuildContext context) { super.build(context); @@ -129,10 +148,11 @@ class _FileManagerPageState extends State backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ - Flexible( - flex: 3, - child: dropArea(FileManagerView( - model.localController, _ffi, _mouseFocusScope))), + if (!isWeb) + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.localController, _ffi, _mouseFocusScope))), Flexible( flex: 3, child: dropArea(FileManagerView( @@ -173,10 +193,31 @@ class _FileManagerPageState extends State /// transfer status list /// watch transfer status Widget statusList() { + Widget getIcon(JobProgress job) { + final color = Theme.of(context).tabBarTheme.labelColor; + switch (job.type) { + case JobType.deleteDir: + case JobType.deleteFile: + return Icon(Icons.delete_outline, color: color); + default: + return Transform.rotate( + angle: isWeb + ? job.isRemoteToLocal + ? pi / 2 + : pi / 2 * 3 + : job.isRemoteToLocal + ? pi + : 0, + child: Icon(Icons.arrow_forward_ios, color: color), + ); + } + } + statusListView(List jobs) => ListView.builder( controller: ScrollController(), itemBuilder: (BuildContext context, int index) { final item = jobs[index]; + final status = item.getStatus(); return Padding( padding: const EdgeInsets.only(bottom: 5), child: generateCard( @@ -186,15 +227,8 @@ class _FileManagerPageState extends State Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Transform.rotate( - angle: item.isRemoteToLocal ? pi : 0, - child: SvgPicture.asset("assets/arrow.svg", - colorFilter: svgColor( - Theme.of(context).tabBarTheme.labelColor)), - ).paddingOnly(left: 15), - const SizedBox( - width: 16.0, - ), + getIcon(item) + .marginSymmetric(horizontal: 10, vertical: 12), Expanded( child: Column( mainAxisSize: MainAxisSize.min, @@ -203,45 +237,28 @@ class _FileManagerPageState extends State Tooltip( waitDuration: Duration(milliseconds: 500), message: item.jobName, - child: Text( - item.fileName, + child: ExtendedText( + item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, - ).paddingSymmetric(vertical: 10), - ), - Text( - '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), - Offstage( - offstage: item.state != JobState.inProgress, - child: Text( - '${translate("Speed")} ${readableFileSize(item.speed)}/s', - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), + overflowWidget: TextOverflowWidget( + child: Text("..."), + position: TextOverflowPosition.start), ), ), - Offstage( - offstage: item.state == JobState.inProgress, - child: Text( - translate( - item.display(), - ), - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - ), + Tooltip( + waitDuration: Duration(milliseconds: 500), + message: status, + child: Text(status, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + )).marginOnly(top: 6), ), Offstage( - offstage: item.state != JobState.inProgress, + offstage: item.type != JobType.transfer || + item.state != JobState.inProgress, child: LinearPercentIndicator( - padding: EdgeInsets.only(right: 15), animateFromLastPercent: true, center: Text( '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', @@ -251,7 +268,7 @@ class _FileManagerPageState extends State progressColor: MyTheme.accent, backgroundColor: Theme.of(context).hoverColor, lineHeight: kDesktopFileTransferRowHeight, - ).paddingSymmetric(vertical: 15), + ).paddingSymmetric(vertical: 8), ), ], ), @@ -276,7 +293,6 @@ class _FileManagerPageState extends State ), MenuButton( tooltip: translate("Delete"), - padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", colorFilter: svgColor(Colors.white), @@ -289,11 +305,11 @@ class _FileManagerPageState extends State hoverColor: MyTheme.accent80, ), ], - ), + ).marginAll(12), ], ), ], - ).paddingSymmetric(vertical: 10), + ), ), ); }, @@ -477,6 +493,9 @@ class _FileManagerViewState extends State { } Widget headTools() { + var uploadButtonTapPosition = RelativeRect.fill; + RxBool isUploadFolder = + (bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs; return Container( child: Column( children: [ @@ -799,6 +818,66 @@ class _FileManagerViewState extends State { ], ), ), + if (isWeb) + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: () => + {webselectFiles(is_folder: isUploadFolder.value)}, + label: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + uploadButtonTapPosition = + RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final value = await showMenu( + context: context, + position: uploadButtonTapPosition, + items: [ + PopupMenuItem( + value: false, + child: Text(translate('Upload files')), + ), + PopupMenuItem( + value: true, + child: Text(translate('Upload folder')), + ), + ]); + if (value != null) { + isUploadFolder.value = value; + bind.mainSetLocalOption( + key: 'upload-folder-button', + value: value ? 'Y' : ''); + webselectFiles(is_folder: value); + } + }, + child: Icon(Icons.arrow_drop_down), + ), + icon: Text( + translate(isUploadFolder.isTrue + ? 'Upload folder' + : 'Upload files'), + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white, + ), + ).marginOnly(left: 8), + )).marginOnly(left: 16), Obx(() => ElevatedButton.icon( style: ButtonStyle( padding: MaterialStateProperty.all( @@ -832,19 +911,22 @@ class _FileManagerViewState extends State { : Colors.white, ), ) - : RotatedBox( - quarterTurns: 2, - child: SvgPicture.asset( - "assets/arrow.svg", - colorFilter: svgColor(selectedItems.items.isEmpty - ? Theme.of(context).brightness == - Brightness.light - ? MyTheme.grayBg - : MyTheme.darkGray - : Colors.white), - alignment: Alignment.bottomRight, - ), - ), + : isWeb + ? Offstage() + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: svgColor( + selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white), + alignment: Alignment.bottomRight, + ), + ), label: isLocal ? SvgPicture.asset( "assets/arrow.svg", @@ -856,7 +938,7 @@ class _FileManagerViewState extends State { : Colors.white), ) : Text( - translate('Receive'), + translate(isWeb ? 'Download' : 'Receive'), style: TextStyle( color: selectedItems.items.isEmpty ? Theme.of(context).brightness == @@ -943,6 +1025,7 @@ class _FileManagerViewState extends State { BuildContext context, ScrollController scrollController) { final fd = controller.directory.value; final entries = fd.entries; + Rx rightClickEntry = Rx(null); return ListSearchActionListener( node: _keyboardNode, @@ -1002,16 +1085,69 @@ class _FileManagerViewState extends State { ? " " : "${entry.lastModified().toString().replaceAll(".000", "")} "; var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0); + onTap() { + final items = selectedItems; + // handle double click + if (_checkDoubleClick(entry)) { + controller.openDirectory(entry.path); + items.clear(); + return; + } + _onSelectedChanged(items, filteredEntries, entry, isLocal); + } + + onSecondaryTap() { + final items = [ + if (!entry.isDrive && + versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0) + mod_menu.PopupMenuItem( + child: Text(translate("Rename")), + height: CustomPopupMenuTheme.height, + onTap: () { + controller.renameAction(entry, isLocal); + }, + ) + ]; + if (items.isNotEmpty) { + rightClickEntry.value = entry; + final future = mod_menu.showMenu( + context: context, + position: secondaryPosition, + items: items, + ); + future.then((value) { + rightClickEntry.value = null; + }); + future.onError((error, stackTrace) { + rightClickEntry.value = null; + }); + } + } + + onSecondaryTapDown(details) { + secondaryPosition = RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy); + } + return Padding( padding: EdgeInsets.symmetric(vertical: 1), child: Obx(() => Container( decoration: BoxDecoration( color: selectedItems.items.contains(entry) - ? Theme.of(context).hoverColor + ? MyTheme.button : Theme.of(context).cardColor, borderRadius: BorderRadius.all( Radius.circular(5.0), ), + border: rightClickEntry.value == entry + ? Border.all( + color: MyTheme.button, + width: 1.0, + ) + : null, ), key: ValueKey(entry.name), height: kDesktopFileTransferRowHeight, @@ -1050,51 +1186,19 @@ class _FileManagerViewState extends State { ), Expanded( child: Text(entry.name.nonBreaking, + style: TextStyle( + color: selectedItems.items + .contains(entry) + ? Colors.white + : null), overflow: TextOverflow.ellipsis)) ]), )), ), - onTap: () { - final items = selectedItems; - // handle double click - if (_checkDoubleClick(entry)) { - controller.openDirectory(entry.path); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, - onSecondaryTap: () { - final items = [ - if (!entry.isDrive && - versionCmp(_ffi.ffiModel.pi.version, - "1.3.0") >= - 0) - mod_menu.PopupMenuItem( - child: Text("Rename"), - height: CustomPopupMenuTheme.height, - onTap: () { - controller.renameAction(entry, isLocal); - }, - ) - ]; - if (items.isNotEmpty) { - mod_menu.showMenu( - context: context, - position: secondaryPosition, - items: items, - ); - } - }, - onSecondaryTapDown: (details) { - secondaryPosition = RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy); - }, + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), SizedBox( width: 2.0, @@ -1111,11 +1215,17 @@ class _FileManagerViewState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, - color: MyTheme.darkGray, + color: selectedItems.items + .contains(entry) + ? Colors.white70 + : MyTheme.darkGray, ), )), ), ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), // Divider from header. SizedBox( @@ -1131,9 +1241,16 @@ class _FileManagerViewState extends State { sizeStr, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), + fontSize: 10, + color: + selectedItems.items.contains(entry) + ? Colors.white70 + : MyTheme.darkGray), ), ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, ), ), ], diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index ca17ac3ff06..cc77cdd9581 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: params['isSharedPassword'], tabController: tabController, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State { super.initState(); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( + debugPrint( "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); // for simplify, just replace connectionId if (call.method == kWindowEventNewFileTransfer) { @@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: args['isSharedPassword'], tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 5541cb8b33b..d6d243c5026 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget { required this.isRDP, required this.isSharedPassword, this.forceRelay, + this.connToken, }) : super(key: key); final String id; final String? password; @@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget { final bool isRDP; final bool? forceRelay; final bool? isSharedPassword; + final String? connToken; @override State createState() => _PortForwardPageState(); @@ -62,6 +64,7 @@ class _PortForwardPageState extends State password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, + connToken: widget.connToken, isRdp: widget.isRDP); Get.put(_ffi, tag: 'pf_${widget.id}'); debugPrint("Port forward page init success with id ${widget.id}"); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 812f7aa99ad..f399f7cab68 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State { tabController: tabController, isRDP: isRDP, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State { isRDP: isRDP, tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 341025c5f3c..cca2074a242 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -115,6 +115,8 @@ class _RemotePageState extends State _ffi.imageModel.addCallbackOnFirstImage((String peerId) { showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); }); _ffi.start( widget.id, @@ -245,13 +247,14 @@ class _RemotePageState extends State super.dispose(); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); _ffi.textureModel.onRemotePageDispose(closeSession); - // ensure we leave this session, this is a double check - _ffi.inputModel.enterOrLeave(false); + if (closeSession) { + // ensure we leave this session, this is a double check + _ffi.inputModel.enterOrLeave(false); + } DesktopMultiWindow.removeListener(this); _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.imageModel.disposeImage(); _ffi.cursorModel.disposeImages(); - _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); await _ffi.close(closeSession: closeSession); _timer?.cancel(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index dc0153da0f4..efd437e1ff7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State { RemoteCountState.find().value = tabController.length; Future _remoteMethodHandler(call, fromWindowId) async { - print( + debugPrint( "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); dynamic returnValue; diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 7828dd4a0ec..0984eea5801 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) { localPlatform = kPeerPlatformWindows; } else if (isLinux) { localPlatform = kPeerPlatformLinux; + } else if (isWebOnWindows || isWebOnLinux) { + localPlatform = kPeerPlatformWebDesktop; } - // to-do: web desktop support ? return localPlatform; } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 98fa676144b..839ea1a81db 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -305,7 +305,7 @@ class RemoteMenuEntry { }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', + translate("Insert Ctrl + Alt + Del"), style: style, ), proc: () { @@ -436,6 +436,7 @@ class _RemoteToolbarState extends State { shadowColor: MyTheme.color(context).shadow, borderRadius: borderRadius, child: _DraggableShowHide( + id: widget.id, sessionId: widget.ffi.sessionId, dragging: _dragging, fractionX: _fractionX, @@ -452,8 +453,8 @@ class _RemoteToolbarState extends State { Widget _buildToolbar(BuildContext context) { final List toolbarItems = []; + toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { - toolbarItems.add(_PinMenu(state: widget.state)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); } @@ -478,8 +479,8 @@ class _RemoteToolbarState extends State { setFullscreen: _setFullscreen, )); toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); if (!isWeb) { - toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); } if (!isWeb) toolbarItems.add(_RecordMenu()); @@ -1612,7 +1613,9 @@ class _KeyboardMenu extends StatelessWidget { // If use flutter to grab keys, we can only use one mode. // Map mode and Legacy mode, at least one of them is supported. String? modeOnly; - if (isInputSourceFlutter) { + // Keep both map and legacy mode on web at the moment. + // TODO: Remove legacy mode after web supports translate mode on web. + if (isInputSourceFlutter && isDesktop) { if (bind.sessionIsKeyboardModeSupported( sessionId: ffi.sessionId, mode: kKeyMapMode)) { modeOnly = kKeyMapMode; @@ -1716,7 +1719,9 @@ class _KeyboardMenu extends StatelessWidget { if (value == null) return; await bind.sessionToggleOption( sessionId: ffi.sessionId, value: kOptionToggleViewOnly); - ffiModel.setViewOnly(id, value); + final viewOnly = await bind.sessionGetToggleOption( + sessionId: ffi.sessionId, arg: kOptionToggleViewOnly); + ffiModel.setViewOnly(id, viewOnly ?? value); } : null, ffi: ffi, @@ -1776,34 +1781,49 @@ class _ChatMenuState extends State<_ChatMenu> { @override Widget build(BuildContext context) { - return _IconSubmenuButton( - tooltip: 'Chat', - key: chatButtonKey, - svg: 'assets/chat.svg', - ffi: widget.ffi, - color: _ToolbarTheme.blueColor, - hoverColor: _ToolbarTheme.hoverBlueColor, - menuChildrenGetter: () => [textChat(), voiceCall()]); + if (isWeb) { + return buildTextChatButton(); + } else { + return _IconSubmenuButton( + tooltip: 'Chat', + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _ToolbarTheme.blueColor, + hoverColor: _ToolbarTheme.hoverBlueColor, + menuChildrenGetter: () => [textChat(), voiceCall()]); + } + } + + buildTextChatButton() { + return _IconMenuButton( + assetName: 'assets/message_24dp_5F6368.svg', + tooltip: 'Text chat', + key: chatButtonKey, + onPressed: _textChatOnPressed, + color: _ToolbarTheme.blueColor, + hoverColor: _ToolbarTheme.hoverBlueColor, + ); } textChat() { return MenuButton( child: Text(translate('Text chat')), ffi: widget.ffi, - onPressed: () { - RenderBox? renderBox = - chatButtonKey.currentContext?.findRenderObject() as RenderBox?; - - Offset? initPos; - if (renderBox != null) { - final pos = renderBox.localToGlobal(Offset.zero); - initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); - } + onPressed: _textChatOnPressed); + } - widget.ffi.chatModel.changeCurrentKey( - MessageKey(widget.ffi.id, ChatModel.clientModeID)); - widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); - }); + _textChatOnPressed() { + RenderBox? renderBox = + chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); + } + widget.ffi.chatModel + .changeCurrentKey(MessageKey(widget.ffi.id, ChatModel.clientModeID)); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); } voiceCall() { @@ -1904,8 +1924,7 @@ class _RecordMenu extends StatelessWidget { var ffi = Provider.of(context); var recordingModel = Provider.of(context); final visible = - (recordingModel.start || ffi.permissions['recording'] != false) && - ffi.pi.currentDisplay != kAllDisplayValue; + (recordingModel.start || ffi.permissions['recording'] != false); if (!visible) return Offstage(); return _IconMenuButton( assetName: 'assets/rec.svg', @@ -2214,6 +2233,7 @@ class RdoMenuButton extends StatelessWidget { } class _DraggableShowHide extends StatefulWidget { + final String id; final SessionID sessionId; final RxDouble fractionX; final RxBool dragging; @@ -2225,6 +2245,7 @@ class _DraggableShowHide extends StatefulWidget { const _DraggableShowHide({ Key? key, + required this.id, required this.sessionId, required this.fractionX, required this.dragging, @@ -2314,15 +2335,33 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); final isFullscreen = stateGlobal.fullscreen; const double iconSize = 20; + + buttonWrapper(VoidCallback? onPressed, Widget child, + {Color hoverColor = _ToolbarTheme.blueColor}) { + final bgColor = buttonStyle.backgroundColor?.resolve({}); + return TextButton( + onPressed: onPressed, + child: child, + style: buttonStyle.copyWith( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.hovered)) { + return (bgColor ?? hoverColor).withOpacity(0.15); + } + return bgColor; + }), + ), + ); + } + final child = Row( mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), - Obx(() => TextButton( - onPressed: () { + Obx(() => buttonWrapper( + () { widget.setFullscreen(!isFullscreen.value); }, - child: Tooltip( + Tooltip( message: translate( isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'), child: Icon( @@ -2333,12 +2372,12 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ), )), - if (!isMacOS) + if (!isMacOS && !isWebDesktop) Obx(() => Offstage( offstage: isFullscreen.isFalse, - child: TextButton( - onPressed: () => widget.setMinimize(), - child: Tooltip( + child: buttonWrapper( + widget.setMinimize, + Tooltip( message: translate('Minimize'), child: Icon( Icons.remove, @@ -2347,11 +2386,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ), )), - TextButton( - onPressed: () => setState(() { + buttonWrapper( + () => setState(() { widget.toolbarState.switchShow(widget.sessionId); }), - child: Obx((() => Tooltip( + Obx((() => Tooltip( message: translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( @@ -2360,6 +2399,25 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ), ))), ), + if (isWebDesktop) + Obx(() { + if (show.isTrue) { + return Offstage(); + } else { + return buttonWrapper( + () => closeConnection(id: widget.id), + Tooltip( + message: translate('Close'), + child: Icon( + Icons.close, + size: iconSize, + color: _ToolbarTheme.redColor, + ), + ), + hoverColor: _ToolbarTheme.redColor, + ).paddingOnly(left: iconSize / 2); + } + }) ], ); return TextButtonTheme( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 0b48b3b92fc..75ecacbfe58 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -552,6 +552,13 @@ class _DesktopTabState extends State controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), children: () { + if (DesktopTabType.cm == tabType) { + // Fix when adding a new tab still showing closed tabs with the same peer id, which would happen after the DesktopTab was stateful. + return state.value.tabs.map((tab) { + return tab.page; + }).toList(); + } + /// to-do refactor, separate connection state and UI state for remote session. /// [workaround] PageView children need an immutable list, after it has been passed into PageView final tabLen = state.value.tabs.length; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c2009bcae13..00afbb001e7 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -36,6 +36,7 @@ WindowType? kWindowType; late List kBootArgs; Future main(List args) async { + earlyAssert(); WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); @@ -161,7 +162,7 @@ void runMobileApp() async { await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); gFFI.userModel.refreshCurrentUser(); runApp(App()); - if (!isWeb) await initUniLinks(); + await initUniLinks(); } void runMultiWindow( @@ -372,7 +373,7 @@ class App extends StatefulWidget { State createState() => _AppState(); } -class _AppState extends State { +class _AppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); @@ -396,6 +397,34 @@ class _AppState extends State { bind.mainChangeTheme(dark: to.toShortString()); } }; + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation()); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + _updateOrientation(); + } + + void _updateOrientation() { + if (isDesktop) return; + + // Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`, + // my test (Flutter 3.19.6, Android 14) is always the reverse value. + // https://github.com/flutter/flutter/issues/60899 + // stateGlobal.isPortrait.value = + // MediaQuery.of(context).orientation == Orientation.portrait; + + final orientation = View.of(context).physicalSize.aspectRatio > 1 + ? Orientation.landscape + : Orientation.portrait; + stateGlobal.isPortrait.value = orientation == Orientation.portrait; } @override @@ -416,7 +445,9 @@ class _AppState extends State { child: GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, - title: 'RustDesk', + title: isWeb + ? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)' + : bind.mainGetAppNameSync(), theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, themeMode: MyTheme.currentThemeMode(), @@ -447,7 +478,8 @@ class _AppState extends State { : (context, child) { child = _keepScaleBuilder(context, child); child = botToastBuilder(context, child); - if (isDesktop && desktopType == DesktopType.main) { + if ((isDesktop && desktopType == DesktopType.main) || + isWebDesktop) { child = keyListenerBuilder(context, child); } if (isLinux) { @@ -475,7 +507,7 @@ _registerEventHandler() { platformFFI.registerEventHandler('theme', 'theme', (evt) async { String? dark = evt['dark']; if (dark != null) { - MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); } }); platformFFI.registerEventHandler('language', 'language', (_) async { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 02c552d716d..89b71c177c9 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -3,25 +3,23 @@ import 'dart:async'; import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import '../../common.dart'; -import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; -import 'scan_page.dart'; -import 'settings_page.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { - ConnectionPage({Key? key}) : super(key: key); + ConnectionPage({Key? key, required this.appBarActions}) : super(key: key); @override final icon = const Icon(Icons.connected_tv); @@ -30,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { final title = translate("Connection"); @override - final appBarActions = isWeb ? [const WebMenu()] : []; + final List appBarActions; @override State createState() => _ConnectionPageState(); @@ -73,9 +71,17 @@ class _ConnectionPageState extends State { } if (isAndroid) { if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + setState(() { + _updateUrl = evt['url']; + }); + } + }); Timer(const Duration(seconds: 1), () async { - _updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (_updateUrl.isNotEmpty) setState(() {}); + bind.mainGetSoftwareUpdateUrl(); }); } } @@ -206,6 +212,8 @@ class _ConnectionPageState extends State { FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { fieldTextEditingController.text = _idController.text; + Get.put( + fieldTextEditingController); fieldFocusNode.addListener(() async { _idEmpty.value = fieldTextEditingController.text.isEmpty; @@ -252,6 +260,9 @@ class _ConnectionPageState extends State { ), ), inputFormatters: [IDTextInputFormatter()], + onSubmitted: (_) { + onConnect(); + }, ); }, onSelected: (option) { @@ -341,9 +352,15 @@ class _ConnectionPageState extends State { ), ), ); + final child = Column(children: [ + if (isWebDesktop) + getConnectionPageTitle(context, true) + .marginOnly(bottom: 10, top: 15, left: 12), + w + ]); return Align( alignment: Alignment.topCenter, - child: Container(constraints: kMobilePageConstraints, child: w)); + child: Container(constraints: kMobilePageConstraints, child: child)); } @override @@ -353,76 +370,13 @@ class _ConnectionPageState extends State { if (Get.isRegistered()) { Get.delete(); } + if (Get.isRegistered()) { + Get.delete(); + } + if (!bind.isCustomClient()) { + platformFFI.unregisterEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); + } super.dispose(); } } - -class WebMenu extends StatefulWidget { - const WebMenu({Key? key}) : super(key: key); - - @override - State createState() => _WebMenuState(); -} - -class _WebMenuState extends State { - @override - Widget build(BuildContext context) { - Provider.of(context); - return PopupMenuButton( - tooltip: "", - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - return (isIOS - ? [ - const PopupMenuItem( - value: "scan", - child: Icon(Icons.qr_code_scanner, color: Colors.black), - ) - ] - : >[]) + - [ - PopupMenuItem( - value: "server", - child: Text(translate('ID/Relay Server')), - ) - ] + - [ - PopupMenuItem( - value: "login", - child: Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})'), - ) - ] + - [ - PopupMenuItem( - value: "about", - child: Text(translate('About RustDesk')), - ) - ]; - }, - onSelected: (value) { - if (value == 'server') { - showServerSettings(gFFI.dialogManager); - } - if (value == 'about') { - showAbout(gFFI.dialogManager); - } - if (value == 'login') { - if (gFFI.userModel.userName.value.isEmpty) { - loginDialog(); - } else { - logOutConfirmDialog(); - } - } - if (value == 'scan') { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ScanPage(), - ), - ); - } - }); - } -} diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 078e2b2f7b3..efccc5de65e 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart'; +import 'package:flutter_hbb/web/settings_page.dart'; import 'package:get/get.dart'; import '../../common.dart'; import '../../common/widgets/chat_page.dart'; import '../../models/platform_model.dart'; +import '../../models/state_model.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { @@ -45,7 +47,11 @@ class HomePageState extends State { void initPages() { _pages.clear(); - if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); + if (!bind.isIncomingOnly()) { + _pages.add(ConnectionPage( + appBarActions: [], + )); + } if (isAndroid && !bind.isOutgoingOnly()) { _chatPageTabIndex = _pages.length; _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); @@ -149,18 +155,80 @@ class HomePageState extends State { } class WebHomePage extends StatelessWidget { - final connectionPage = ConnectionPage(); + final connectionPage = + ConnectionPage(appBarActions: [const WebSettingsPage()]); @override Widget build(BuildContext context) { + stateGlobal.isInMainPage = true; + handleUnilink(context); return Scaffold( // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, - title: Text(bind.mainGetAppNameSync()), + title: Text("${bind.mainGetAppNameSync()} (Preview)"), actions: connectionPage.appBarActions, ), body: connectionPage, ); } + + handleUnilink(BuildContext context) { + if (webInitialLink.isEmpty) { + return; + } + final link = webInitialLink; + webInitialLink = ''; + final splitter = ["/#/", "/#", "#/", "#"]; + var fakelink = ''; + for (var s in splitter) { + if (link.contains(s)) { + var list = link.split(s); + if (list.length < 2 || list[1].isEmpty) { + return; + } + list.removeAt(0); + fakelink = "rustdesk://${list.join(s)}"; + break; + } + } + if (fakelink.isEmpty) { + return; + } + final uri = Uri.tryParse(fakelink); + if (uri == null) { + return; + } + final args = urlLinkToCmdArgs(uri); + if (args == null || args.isEmpty) { + return; + } + bool isFileTransfer = 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; + case '--file-transfer': + isFileTransfer = true; + id = args[i + 1]; + i++; + break; + case '--password': + password = args[i + 1]; + i++; + break; + default: + break; + } + } + if (id != null) { + connect(context, id, isFileTransfer: isFileTransfer, password: password); + } + } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 74b56cd45fc..40890f228e6 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -57,6 +57,9 @@ class _RemotePageState extends State { final TextEditingController _textController = TextEditingController(text: initText); + // This timer is used to check the composing status of the soft keyboard. + // It is used for Android, Korean(and other similar) input method. + Timer? _composingTimer; _RemotePageState(String id) { initSharedStates(id); @@ -89,6 +92,13 @@ class _RemotePageState extends State { gFFI.chatModel .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); _blockableOverlayState.applyFfi(gFFI); + gFFI.imageModel.addCallbackOnFirstImage((String peerId) { + gFFI.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId)); + if (gFFI.recordingModel.start) { + showToast(translate('Automatically record outgoing sessions')); + } + }); } @override @@ -104,6 +114,7 @@ class _RemotePageState extends State { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); + _composingTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -139,6 +150,7 @@ class _RemotePageState extends State { gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.invokeMethod("enable_soft_keyboard", false); } + _composingTimer?.cancel(); } else { _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { @@ -155,9 +167,9 @@ class _RemotePageState extends State { var oldValue = _value; _value = newValue; var i = newValue.length - 1; - for (; i >= 0 && newValue[i] != '\1'; --i) {} + for (; i >= 0 && newValue[i] != '1'; --i) {} var j = oldValue.length - 1; - for (; j >= 0 && oldValue[j] != '\1'; --j) {} + for (; j >= 0 && oldValue[j] != '1'; --j) {} if (i < j) j = i; var subNewValue = newValue.substring(j + 1); var subOldValue = oldValue.substring(j + 1); @@ -202,12 +214,19 @@ class _RemotePageState extends State { } void _handleNonIOSSoftKeyboardInput(String newValue) { + _composingTimer?.cancel(); + if (_textController.value.isComposingRangeValid) { + _composingTimer = Timer(Duration(milliseconds: 25), () { + _handleNonIOSSoftKeyboardInput(_textController.value.text); + }); + return; + } var oldValue = _value; _value = newValue; if (oldValue.isNotEmpty && newValue.isNotEmpty && - oldValue[0] == '\1' && - newValue[0] != '\1') { + oldValue[0] == '1' && + newValue[0] != '1') { // clipboard oldValue = ''; } @@ -242,10 +261,14 @@ class _RemotePageState extends State { } } - // handle mobile virtual keyboard - void handleSoftKeyboardInput(String newValue) { + Future handleSoftKeyboardInput(String newValue) async { if (isIOS) { - _handleIOSSoftKeyboardInput(newValue); + // fix: TextFormField onChanged event triggered multiple times when Korean input + // https://github.com/rustdesk/rustdesk/pull/9644 + await Future.delayed(const Duration(milliseconds: 10)); + + if (newValue != _textController.text) return; + _handleIOSSoftKeyboardInput(_textController.text); } else { _handleNonIOSSoftKeyboardInput(newValue); } diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 527e7446ff2..e92400dba6d 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget { class _ScanPageState extends State { QRViewController? controller; final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); + StreamSubscription? scanSubscription; - // In order to get hot reload to work we need to pause the camera if the platform - // is android, or resume the camera if the platform is iOS. @override void reassemble() { super.reassemble(); - if (isAndroid) { + if (isAndroid && controller != null) { controller!.pauseCamera(); + } else if (controller != null) { + controller!.resumeCamera(); } - controller!.resumeCamera(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Scan QR'), - actions: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.image_search), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - final XFile? file = - await picker.pickImage(source: ImageSource.gallery); - if (file != null) { - var image = img.decodeNamedImage( - file.path, File(file.path).readAsBytesSync())!; - - LuminanceSource source = RGBLuminanceSource( - image.width, - image.height, - image - .getBytes(order: img.ChannelOrder.abgr) - .buffer - .asInt32List()); - var bitmap = BinaryBitmap(HybridBinarizer(source)); - - var reader = QRCodeReader(); - try { - var result = reader.decode(bitmap); - if (result.text.startsWith(bind.mainUriPrefixSync())) { - handleUriLink(uriString: result.text); - } else { - showServerSettingFromQr(result.text); - } - } catch (e) { - showToast('No QR code found'); - } - } - }), - IconButton( - color: Colors.yellow, - icon: Icon(Icons.flash_on), - iconSize: 32.0, - onPressed: () async { - await controller?.toggleFlash(); - }), - IconButton( - color: Colors.white, - icon: Icon(Icons.switch_camera), - iconSize: 32.0, - onPressed: () async { - await controller?.flipCamera(); - }, - ), - ], - ), - body: _buildQrView(context)); + appBar: AppBar( + title: const Text('Scan QR'), + actions: [ + _buildImagePickerButton(), + _buildFlashToggleButton(), + _buildCameraSwitchButton(), + ], + ), + body: _buildQrView(context), + ); } Widget _buildQrView(BuildContext context) { - // For this example we check how width or tall the device is and change the scanArea and overlay accordingly. - var scanArea = (MediaQuery.of(context).size.width < 400 || - MediaQuery.of(context).size.height < 400) + var scanArea = MediaQuery.of(context).size.width < 400 || + MediaQuery.of(context).size.height < 400 ? 150.0 : 300.0; - // To ensure the Scanner view is properly sizes after rotation - // we need to listen for Flutter SizeChanged notification and update controller return QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( - borderColor: Colors.red, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: scanArea), + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), ); } @@ -116,7 +69,7 @@ class _ScanPageState extends State { setState(() { this.controller = controller; }); - controller.scannedDataStream.listen((scanData) { + scanSubscription = controller.scannedDataStream.listen((scanData) { if (scanData.code != null) { showServerSettingFromQr(scanData.code!); } @@ -129,8 +82,66 @@ class _ScanPageState extends State { } } + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + final XFile? file = await picker.pickImage(source: ImageSource.gallery); + if (file != null) { + try { + var image = img.decodeImage(await File(file.path).readAsBytes())!; + LuminanceSource source = RGBLuminanceSource( + image.width, + image.height, + image.getBytes(order: img.ChannelOrder.abgr).buffer.asInt32List(), + ); + var bitmap = BinaryBitmap(HybridBinarizer(source)); + + var reader = QRCodeReader(); + var result = reader.decode(bitmap); + if (result.text.startsWith(bind.mainUriPrefixSync())) { + handleUriLink(uriString: result.text); + } else { + showServerSettingFromQr(result.text); + } + } catch (e) { + showToast('No QR code found'); + } + } + } + + Widget _buildImagePickerButton() { + return IconButton( + color: Colors.white, + icon: Icon(Icons.image_search), + iconSize: 32.0, + onPressed: _pickImage, + ); + } + + Widget _buildFlashToggleButton() { + return IconButton( + color: Colors.yellow, + icon: Icon(Icons.flash_on), + iconSize: 32.0, + onPressed: () async { + await controller?.toggleFlash(); + }, + ); + } + + Widget _buildCameraSwitchButton() { + return IconButton( + color: Colors.white, + icon: Icon(Icons.switch_camera), + iconSize: 32.0, + onPressed: () async { + await controller?.flipCamera(); + }, + ); + } + @override void dispose() { + scanSubscription?.cancel(); controller?.dispose(); super.dispose(); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 8fac2ea2a04..eb386593314 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -79,6 +79,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _enableRecordSession = false; var _enableHardwareCodec = false; var _autoRecordIncomingSession = false; + var _autoRecordOutgoingSession = false; var _allowAutoDisconnect = false; var _localIP = ""; var _directAccessPort = ""; @@ -104,6 +105,8 @@ class _SettingsState extends State with WidgetsBindingObserver { bind.mainGetOptionSync(key: kOptionEnableHwcodec)); _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming, bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming)); + _autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing, + bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing)); _localIP = bind.mainGetOptionSync(key: 'local-ip-addr'); _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort); _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect, @@ -231,6 +234,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { Provider.of(context); final outgoingOnly = bind.isOutgoingOnly(); + final incommingOnly = bind.isIncomingOnly(); final customClientSection = CustomSettingsSection( child: Column( children: [ @@ -674,32 +678,55 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ), ]), - if (isAndroid && !outgoingOnly) + if (isAndroid) SettingsSection( title: Text(translate("Recording")), tiles: [ - SettingsTile.switchTile( - title: - Text(translate('Automatically record incoming sessions')), - leading: Icon(Icons.videocam), - description: Text( - "${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"), - initialValue: _autoRecordIncomingSession, - onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) - ? null - : (v) async { - await bind.mainSetOption( - key: kOptionAllowAutoRecordIncoming, - value: - bool2option(kOptionAllowAutoRecordIncoming, v)); - final newValue = option2bool( - kOptionAllowAutoRecordIncoming, - await bind.mainGetOption( - key: kOptionAllowAutoRecordIncoming)); - setState(() { - _autoRecordIncomingSession = newValue; - }); - }, + if (!outgoingOnly) + SettingsTile.switchTile( + title: + Text(translate('Automatically record incoming sessions')), + initialValue: _autoRecordIncomingSession, + onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming) + ? null + : (v) async { + await bind.mainSetOption( + key: kOptionAllowAutoRecordIncoming, + value: bool2option( + kOptionAllowAutoRecordIncoming, v)); + final newValue = option2bool( + kOptionAllowAutoRecordIncoming, + await bind.mainGetOption( + key: kOptionAllowAutoRecordIncoming)); + setState(() { + _autoRecordIncomingSession = newValue; + }); + }, + ), + if (!incommingOnly) + SettingsTile.switchTile( + title: + Text(translate('Automatically record outgoing sessions')), + initialValue: _autoRecordOutgoingSession, + onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing) + ? null + : (v) async { + await bind.mainSetLocalOption( + key: kOptionAllowAutoRecordOutgoing, + value: bool2option( + kOptionAllowAutoRecordOutgoing, v)); + final newValue = option2bool( + kOptionAllowAutoRecordOutgoing, + bind.mainGetLocalOption( + key: kOptionAllowAutoRecordOutgoing)); + setState(() { + _autoRecordOutgoingSession = newValue; + }); + }, + ), + SettingsTile( + title: Text(translate("Directory")), + description: Text(bind.mainVideoSaveDirectory(root: false)), ), ], ), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 391bec669d5..2d17f3b5438 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -205,14 +205,15 @@ void showServerSettingsWithValue( ) ] + [ - TextFormField( - controller: relayCtrl, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg.value.isEmpty - ? null - : relayServerMsg.value), - ) + if (isAndroid) + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg.value.isEmpty + ? null + : relayServerMsg.value), + ) ] + [ TextFormField( diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 6f3820e86bb..0da84e0f26c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -66,10 +66,16 @@ class AbModel { var listInitialized = false; var _maxPeerOneAb = 0; + late final Peers peersModel; + WeakReference parent; AbModel(this.parent) { addressbooks.clear(); + peersModel = Peers( + name: PeersModelName.addressBook, + getInitPeers: () => currentAbPeers, + loadEvent: LoadEvent.addressBook); if (desktopType == DesktopType.main) { Timer.periodic(Duration(milliseconds: 500), (timer) async { if (_timerCounter++ % 6 == 0) { diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 778c9435704..d79c9f07022 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -235,13 +235,14 @@ class ChatModel with ChangeNotifier { } } - _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || - chatWindowOverlayEntry == null); + _isChatOverlayHide() => + ((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) || + chatWindowOverlayEntry == null); toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); - if (!isDesktop) { + if (!(isDesktop || isWebDesktop)) { showChatIconOverlay(); } showChatWindowOverlay(chatInitPos: chatInitPos); diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index ab8df3c4539..c6cf55256de 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -181,6 +181,7 @@ class TextureModel { } updateCurrentDisplay(int curDisplay) { + if (isWeb) return; final ffi = parent.target; if (ffi == null) return; tryCreateTexture(int idx) { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 0838c8b0673..05c79ae86a7 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -7,6 +7,8 @@ import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; +import 'package:flutter_hbb/web/dummy.dart' + if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; import '../consts.dart'; import 'model.dart'; @@ -34,6 +36,7 @@ class JobID { } typedef GetSessionID = SessionID Function(); +typedef GetDialogManager = OverlayDialogManager? Function(); class FileModel { final WeakReference parent; @@ -45,13 +48,15 @@ class FileModel { late final FileController remoteController; late final GetSessionID getSessionID; + late final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); late final FileDialogEventLoop evtLoop; FileModel(this.parent) { getSessionID = () => parent.target!.sessionId; + getDialogManager = () => parent.target?.dialogManager; fileFetcher = FileFetcher(getSessionID); - jobController = JobController(getSessionID); + jobController = JobController(getSessionID, getDialogManager); localController = FileController( isLocal: true, getSessionID: getSessionID, @@ -71,7 +76,7 @@ class FileModel { Future onReady() async { await evtLoop.onReady(); - await localController.onReady(); + if (!isWeb) await localController.onReady(); await remoteController.onReady(); } @@ -83,7 +88,7 @@ class FileModel { } Future refreshAll() async { - await localController.refresh(); + if (!isWeb) await localController.refresh(); await remoteController.refresh(); } @@ -225,6 +230,33 @@ class FileModel { ); }, useAnimation: false); } + + void onSelectedFiles(dynamic obj) { + localController.selectedItems.clear(); + + try { + int handleIndex = int.parse(obj['handleIndex']); + final file = jsonDecode(obj['file']); + var entry = Entry.fromJson(file); + entry.path = entry.name; + final otherSideData = remoteController.directoryData(); + final toPath = otherSideData.directory.path; + final isWindows = otherSideData.options.isWindows; + final showHidden = otherSideData.options.showHidden; + final jobID = jobController.addTransferJob(entry, false); + webSendLocalFiles( + handleIndex: handleIndex, + actId: jobID, + path: entry.path, + to: PathUtil.join(toPath, entry.name, isWindows), + fileNum: 0, + includeHidden: showHidden, + isRemote: false, + ); + } catch (e) { + debugPrint("Failed to decode onSelectedFiles: $e"); + } + } } class DirectoryData { @@ -451,7 +483,7 @@ class FileController { final isWindows = otherSideData.options.isWindows; final showHidden = otherSideData.options.showHidden; for (var from in items.items) { - final jobID = jobController.add(from, isRemoteToLocal); + final jobID = jobController.addTransferJob(from, isRemoteToLocal); bind.sessionSendFiles( sessionId: sessionId, actId: jobID, @@ -459,7 +491,8 @@ class FileController { to: PathUtil.join(toPath, from.name, isWindows), fileNum: 0, includeHidden: showHidden, - isRemote: isRemoteToLocal); + isRemote: isRemoteToLocal, + isDir: from.isDirectory); debugPrint( "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); } @@ -486,7 +519,7 @@ class FileController { } else if (item.isDirectory) { title = translate("Not an empty directory"); dialogManager?.showLoading(translate("Waiting")); - final fd = await fileFetcher.fetchDirectoryRecursive( + final fd = await fileFetcher.fetchDirectoryRecursiveToRemove( jobID, item.path, items.isLocal, true); if (fd.path.isEmpty) { fd.path = item.path; @@ -494,13 +527,21 @@ class FileController { fd.format(isWindows); dialogManager?.dismissAll(); if (fd.entries.isEmpty) { + var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0); final confirm = await showRemoveDialog( translate( "Are you sure you want to delete this empty directory?"), item.name, false); if (confirm == true) { - sendRemoveEmptyDir(item.path, 0); + sendRemoveEmptyDir( + item.path, + 0, + deleteJobId, + ); + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", state: JobState.done); } return; } @@ -508,6 +549,13 @@ class FileController { } else { entries = []; } + int deleteJobId; + if (item.isDirectory) { + deleteJobId = + jobController.addDeleteDirJob(item, !isLocal, entries.length); + } else { + deleteJobId = jobController.addDeleteFileJob(item, !isLocal); + } for (var i = 0; i < entries.length; i++) { final dirShow = item.isDirectory @@ -522,24 +570,32 @@ class FileController { ); try { if (confirm == true) { - sendRemoveFile(entries[i].path, i); + sendRemoveFile(entries[i].path, i, deleteJobId); final res = await jobController.jobResultListener.start(); // handle remove res; if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } + } else { + jobController.updateJobStatus(deleteJobId, + file_num: i, error: "cancel"); } if (_removeCheckboxRemember) { if (confirm == true) { for (var j = i + 1; j < entries.length; j++) { - sendRemoveFile(entries[j].path, j); + sendRemoveFile(entries[j].path, j, deleteJobId); final res = await jobController.jobResultListener.start(); if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i); + sendRemoveEmptyDir(item.path, i, deleteJobId); } } + } else { + jobController.updateJobStatus(deleteJobId, + error: "cancel", + file_num: entries.length, + state: JobState.done); } break; } @@ -618,22 +674,19 @@ class FileController { }, useAnimation: false); } - void sendRemoveFile(String path, int fileNum) { + void sendRemoveFile(String path, int fileNum, int actId) { bind.sessionRemoveFile( sessionId: sessionId, - actId: JobController.jobID.next(), + actId: actId, path: path, isRemote: !isLocal, fileNum: fileNum); } - void sendRemoveEmptyDir(String path, int fileNum) { + void sendRemoveEmptyDir(String path, int fileNum, int actId) { history.removeWhere((element) => element.contains(path)); bind.sessionRemoveAllEmptyDirs( - sessionId: sessionId, - actId: JobController.jobID.next(), - path: path, - isRemote: !isLocal); + sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } Future createDir(String path) async { @@ -716,27 +769,29 @@ class FileController { } } +const _kOneWayFileTransferError = 'one-way-file-transfer-tip'; + class JobController { static final JobID jobID = JobID(); final jobTable = List.empty(growable: true).obs; final jobResultListener = JobResultListener>(); final GetSessionID getSessionID; + final GetDialogManager getDialogManager; SessionID get sessionId => getSessionID(); + OverlayDialogManager? get alogManager => getDialogManager(); + int _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch; - JobController(this.getSessionID); + JobController(this.getSessionID, this.getDialogManager); int getJob(int id) { return jobTable.indexWhere((element) => element.id == id); } - // JobProgress? getJob(int id) { - // return jobTable.firstWhere((element) => element.id == id); - // } - // return jobID - int add(Entry from, bool isRemoteToLocal) { + int addTransferJob(Entry from, bool isRemoteToLocal) { final jobID = JobController.jobID.next(); jobTable.add(JobProgress() + ..type = JobType.transfer ..fileName = path.basename(from.path) ..jobName = from.path ..totalSize = from.size @@ -746,6 +801,33 @@ class JobController { return jobID; } + int addDeleteFileJob(Entry file, bool isRemote) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteFile + ..fileName = path.basename(file.path) + ..jobName = file.path + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + + int addDeleteDirJob(Entry file, bool isRemote, int fileCount) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..type = JobType.deleteDir + ..fileName = path.basename(file.path) + ..jobName = file.path + ..fileCount = fileCount + ..totalSize = file.size + ..state = JobState.none + ..id = jobID + ..isRemoteToLocal = isRemote); + return jobID; + } + void tryUpdateJobProgress(Map evt) { try { int id = int.parse(evt['id']); @@ -756,7 +838,7 @@ class JobController { job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); - debugPrint("update job $id with $evt"); + job.recvJobRes = true; jobTable.refresh(); } } catch (e) { @@ -764,20 +846,48 @@ class JobController { } } - void jobDone(Map evt) async { + Future jobDone(Map evt) async { if (jobResultListener.isListening) { jobResultListener.complete(evt); - return; + // return; } - - int id = int.parse(evt['id']); + int id = -1; + int? fileNum = 0; + double? speed = 0; + try { + id = int.parse(evt['id']); + } catch (_) {} final jobIndex = getJob(id); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - job.finishedSize = job.totalSize; + if (jobIndex == -1) return true; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (job.type == JobType.deleteFile) { + job.state = JobState.done; + } else if (job.type == JobType.deleteDir) { + try { + fileNum = int.tryParse(evt['file_num']); + } catch (_) {} + if (fileNum != null) { + if (fileNum < job.fileNum) return true; // file_num can be 0 at last + job.fileNum = fileNum; + if (fileNum >= job.fileCount - 1) { + job.state = JobState.done; + } + } + } else { + try { + fileNum = int.tryParse(evt['file_num']); + speed = double.tryParse(evt['speed']); + } catch (_) {} + if (fileNum != null) job.fileNum = fileNum; + if (speed != null) job.speed = speed; job.state = JobState.done; - job.fileNum = int.parse(evt['file_num']); - jobTable.refresh(); + } + jobTable.refresh(); + if (job.type == JobType.deleteDir) { + return job.state == JobState.done; + } else { + return true; } } @@ -788,16 +898,61 @@ class JobController { final job = jobTable[jobIndex]; job.state = JobState.error; job.err = err; - job.fileNum = int.parse(evt['file_num']); - if (err == "skipped") { - job.state = JobState.done; - job.finishedSize = job.totalSize; + job.recvJobRes = true; + if (job.type == JobType.transfer) { + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + if (err == "skipped") { + job.state = JobState.done; + job.finishedSize = job.totalSize; + } + } else if (job.type == JobType.deleteDir) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } + int? fileNum = int.tryParse(evt['file_num']); + if (fileNum != null) job.fileNum = fileNum; + } else if (job.type == JobType.deleteFile) { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + } } jobTable.refresh(); } + if (err == _kOneWayFileTransferError) { + if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) { + final dm = alogManager; + if (dm != null) { + _lastTimeShowMsgbox = DateTime.now().millisecondsSinceEpoch; + msgBox(sessionId, 'custom-nocancel', 'Error', err, '', dm); + } + } + } debugPrint("jobError $evt"); } + void updateJobStatus(int id, + {int? file_num, String? error, JobState? state}) { + final jobIndex = getJob(id); + if (jobIndex < 0) return; + final job = jobTable[jobIndex]; + job.recvJobRes = true; + if (file_num != null) { + job.fileNum = file_num; + } + if (error != null) { + job.err = error; + job.state = JobState.error; + } + if (state != null) { + job.state = state; + } + if (job.type == JobType.deleteFile && error == null) { + job.state = JobState.done; + } + jobTable.refresh(); + } + Future cancelJob(int id) async { await bind.sessionCancelJob(sessionId: sessionId, actId: id); } @@ -814,6 +969,7 @@ class JobController { final currJobId = JobController.jobID.next(); String fileName = path.basename(isRemote ? remote : to); var jobProgress = JobProgress() + ..type = JobType.transfer ..fileName = fileName ..jobName = isRemote ? remote : to ..id = currJobId @@ -989,11 +1145,11 @@ class FileFetcher { } } - Future fetchDirectoryRecursive( + Future fetchDirectoryRecursiveToRemove( int actID, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { - await bind.sessionReadDirRecursive( + await bind.sessionReadDirToRemoveRecursive( sessionId: sessionId, actId: actID, path: path, @@ -1088,8 +1244,12 @@ extension JobStateDisplay on JobState { } } +enum JobType { none, transfer, deleteFile, deleteDir } + class JobProgress { + JobType type = JobType.none; JobState state = JobState.none; + var recvJobRes = false; var id = 0; var fileNum = 0; var speed = 0.0; @@ -1109,7 +1269,9 @@ class JobProgress { int lastTransferredSize = 0; clear() { + type = JobType.none; state = JobState.none; + recvJobRes = false; id = 0; fileNum = 0; speed = 0; @@ -1123,11 +1285,81 @@ class JobProgress { } String display() { - if (state == JobState.done && err == "skipped") { - return translate("Skipped"); + if (type == JobType.transfer) { + if (state == JobState.done && err == "skipped") { + return translate("Skipped"); + } + } else if (type == JobType.deleteFile) { + if (err == "cancel") { + return translate("Cancel"); + } } + return state.display(); } + + String getStatus() { + int handledFileCount = recvJobRes ? fileNum + 1 : fileNum; + if (handledFileCount >= fileCount) { + handledFileCount = fileCount; + } + if (state == JobState.done) { + handledFileCount = fileCount; + finishedSize = totalSize; + } + final filesStr = "$handledFileCount/$fileCount files"; + final sizeStr = totalSize > 0 ? readableFileSize(totalSize.toDouble()) : ""; + final sizePercentStr = totalSize > 0 && finishedSize > 0 + ? "${readableFileSize(finishedSize.toDouble())} / ${readableFileSize(totalSize.toDouble())}" + : ""; + if (type == JobType.deleteFile) { + return display(); + } else if (type == JobType.deleteDir) { + var res = ''; + if (state == JobState.done || state == JobState.error) { + res = display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += " "; + } + res += filesStr; + } + + if (sizeStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + return res; + } else if (type == JobType.transfer) { + var res = ""; + if (state != JobState.inProgress && state != JobState.none) { + res += display(); + } + if (filesStr.isNotEmpty) { + if (res.isNotEmpty) { + res += ", "; + } + res += filesStr; + } + if (sizeStr.isNotEmpty && state != JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizeStr; + } + if (sizePercentStr.isNotEmpty && state == JobState.inProgress) { + if (res.isNotEmpty) { + res += ", "; + } + res += sizePercentStr; + } + return res; + } + return ''; + } } class _PathStat { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 184c94bfff3..b14ccd46b0e 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -23,7 +23,14 @@ class GroupModel { bool get emtpy => users.isEmpty && peers.isEmpty; - GroupModel(this.parent); + late final Peers peersModel; + + GroupModel(this.parent) { + peersModel = Peers( + name: PeersModelName.group, + getInitPeers: () => peers, + loadEvent: LoadEvent.group); + } Future pull({force = true, quiet = false}) async { if (bind.isDisableGroupPanel()) return; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index dde815789ae..c7e1e6131c3 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -177,7 +177,7 @@ class PointerEventToRust { } } -class ToReleaseKeys { +class ToReleaseRawKeys { RawKeyEvent? lastLShiftKeyEvent; RawKeyEvent? lastRShiftKeyEvent; RawKeyEvent? lastLCtrlKeyEvent; @@ -282,6 +282,48 @@ class ToReleaseKeys { } } +class ToReleaseKeys { + KeyEvent? lastLShiftKeyEvent; + KeyEvent? lastRShiftKeyEvent; + KeyEvent? lastLCtrlKeyEvent; + KeyEvent? lastRCtrlKeyEvent; + KeyEvent? lastLAltKeyEvent; + KeyEvent? lastRAltKeyEvent; + KeyEvent? lastLCommandKeyEvent; + KeyEvent? lastRCommandKeyEvent; + KeyEvent? lastSuperKeyEvent; + + reset() { + lastLShiftKeyEvent = null; + lastRShiftKeyEvent = null; + lastLCtrlKeyEvent = null; + lastRCtrlKeyEvent = null; + lastLAltKeyEvent = null; + lastRAltKeyEvent = null; + lastLCommandKeyEvent = null; + lastRCommandKeyEvent = null; + lastSuperKeyEvent = null; + } + + release(KeyEventResult Function(KeyEvent e) handleKeyEvent) { + for (final key in [ + lastLShiftKeyEvent, + lastRShiftKeyEvent, + lastLCtrlKeyEvent, + lastRCtrlKeyEvent, + lastLAltKeyEvent, + lastRAltKeyEvent, + lastLCommandKeyEvent, + lastRCommandKeyEvent, + lastSuperKeyEvent, + ]) { + if (key != null) { + handleKeyEvent(key); + } + } + } +} + class InputModel { final WeakReference parent; String keyboardMode = ''; @@ -292,6 +334,7 @@ class InputModel { var alt = false; var command = false; + final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys(); final ToReleaseKeys toReleaseKeys = ToReleaseKeys(); // trackpad @@ -339,10 +382,99 @@ class InputModel { } } + void handleKeyDownEventModifiers(KeyEvent e) { + KeyUpEvent upEvent(e) => KeyUpEvent( + physicalKey: e.physicalKey, + logicalKey: e.logicalKey, + timeStamp: e.timeStamp, + ); + if (e.logicalKey == LogicalKeyboardKey.altLeft) { + if (!alt) { + alt = true; + } + toReleaseKeys.lastLAltKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.altRight) { + if (!alt) { + alt = true; + } + toReleaseKeys.lastLAltKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.controlLeft) { + if (!ctrl) { + ctrl = true; + } + toReleaseKeys.lastLCtrlKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.controlRight) { + if (!ctrl) { + ctrl = true; + } + toReleaseKeys.lastRCtrlKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) { + if (!shift) { + shift = true; + } + toReleaseKeys.lastLShiftKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.shiftRight) { + if (!shift) { + shift = true; + } + toReleaseKeys.lastRShiftKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.metaLeft) { + if (!command) { + command = true; + } + toReleaseKeys.lastLCommandKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.metaRight) { + if (!command) { + command = true; + } + toReleaseKeys.lastRCommandKeyEvent = upEvent(e); + } else if (e.logicalKey == LogicalKeyboardKey.superKey) { + if (!command) { + command = true; + } + toReleaseKeys.lastSuperKeyEvent = upEvent(e); + } + } + + void handleKeyUpEventModifiers(KeyEvent e) { + if (e.logicalKey == LogicalKeyboardKey.altLeft) { + alt = false; + toReleaseKeys.lastLAltKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.altRight) { + alt = false; + toReleaseKeys.lastRAltKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.controlLeft) { + ctrl = false; + toReleaseKeys.lastLCtrlKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.controlRight) { + ctrl = false; + toReleaseKeys.lastRCtrlKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) { + shift = false; + toReleaseKeys.lastLShiftKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.shiftRight) { + shift = false; + toReleaseKeys.lastRShiftKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.metaLeft) { + command = false; + toReleaseKeys.lastLCommandKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.metaRight) { + command = false; + toReleaseKeys.lastRCommandKeyEvent = null; + } else if (e.logicalKey == LogicalKeyboardKey.superKey) { + command = false; + toReleaseKeys.lastSuperKeyEvent = null; + } + } + KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; - if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { - return KeyEventResult.handled; + if (!isInputSourceFlutter) { + if (isDesktop) { + return KeyEventResult.handled; + } else if (isWeb) { + return KeyEventResult.ignored; + } } final key = e.logicalKey; @@ -358,7 +490,7 @@ class InputModel { command = true; } } - toReleaseKeys.updateKeyDown(key, e); + toReleaseRawKeys.updateKeyDown(key, e); } if (e is RawKeyUpEvent) { if (key == LogicalKeyboardKey.altLeft || @@ -376,12 +508,49 @@ class InputModel { command = false; } - toReleaseKeys.updateKeyUp(key, e); + toReleaseRawKeys.updateKeyUp(key, e); } // * Currently mobile does not enable map mode - if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { - mapKeyboardMode(e); + if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { + mapKeyboardModeRaw(e); + } else { + legacyKeyboardModeRaw(e); + } + + return KeyEventResult.handled; + } + + KeyEventResult handleKeyEvent(KeyEvent e) { + if (isViewOnly) return KeyEventResult.handled; + if (!isInputSourceFlutter) { + if (isDesktop) { + return KeyEventResult.handled; + } else if (isWeb) { + return KeyEventResult.ignored; + } + } + if (isWindows || isLinux) { + // Ignore meta keys. Because flutter window will loose focus if meta key is pressed. + if (e.physicalKey == PhysicalKeyboardKey.metaLeft || + e.physicalKey == PhysicalKeyboardKey.metaRight) { + return KeyEventResult.handled; + } + } + + if (e is KeyUpEvent) { + handleKeyUpEventModifiers(e); + } else if (e is KeyDownEvent) { + handleKeyDownEventModifiers(e); + } + + if (isMobile || (isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { + // FIXME: e.character is wrong for dead keys, eg: ^ in de + newKeyboardMode( + e.character ?? '', + e.physicalKey.usbHidUsage & 0xFFFF, + // Show repeat event be converted to "release+press" events? + e is KeyDownEvent || e is KeyRepeatEvent); } else { legacyKeyboardMode(e); } @@ -389,7 +558,33 @@ class InputModel { return KeyEventResult.handled; } - void mapKeyboardMode(RawKeyEvent e) { + /// Send Key Event + void newKeyboardMode(String character, int usbHid, bool down) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } + bind.sessionHandleFlutterKeyEvent( + sessionId: sessionId, + character: character, + usbHid: usbHid, + lockModes: lockModes, + downOrUp: down); + } + + void mapKeyboardModeRaw(RawKeyEvent e) { int positionCode = -1; int platformCode = -1; bool down; @@ -441,7 +636,7 @@ class InputModel { .contains(KeyboardLockMode.scrollLock)) { lockModes |= (1 << scrolllock); } - bind.sessionHandleFlutterKeyEvent( + bind.sessionHandleFlutterRawKeyEvent( sessionId: sessionId, name: name, platformCode: platformCode, @@ -450,7 +645,7 @@ class InputModel { downOrUp: down); } - void legacyKeyboardMode(RawKeyEvent e) { + void legacyKeyboardModeRaw(RawKeyEvent e) { if (e is RawKeyDownEvent) { if (e.repeat) { sendRawKey(e, press: true); @@ -471,6 +666,24 @@ class InputModel { inputKey(label, down: down, press: press ?? false); } + void legacyKeyboardMode(KeyEvent e) { + if (e is KeyDownEvent) { + sendKey(e, down: true); + } else if (e is KeyRepeatEvent) { + sendKey(e, press: true); + } else if (e is KeyUpEvent) { + sendKey(e); + } + } + + void sendKey(KeyEvent e, {bool? down, bool? press}) { + // for maximum compatibility + final label = physicalKeyMap[e.physicalKey.usbHidUsage] ?? + logicalKeyMap[e.logicalKey.keyId] ?? + e.logicalKey.keyLabel; + inputKey(label, down: down, press: press ?? false); + } + /// Send key stroke event. /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). @@ -566,7 +779,8 @@ class InputModel { } void enterOrLeave(bool enter) { - toReleaseKeys.release(handleRawKeyEvent); + toReleaseKeys.release(handleKeyEvent); + toReleaseRawKeys.release(handleRawKeyEvent); _pointerMovedAfterEnter = false; // Fix status @@ -577,6 +791,9 @@ class InputModel { if (!isInputSourceFlutter) { bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); } + if (!isWeb && enter) { + bind.setCurSessionId(sessionId: sessionId); + } } /// Send mouse movement event with distance in [x] and [y]. @@ -1164,15 +1381,15 @@ class InputModel { // Simulate a key press event. // `usbHidUsage` is the USB HID usage code of the key. Future tapHidKey(int usbHidUsage) async { - inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, true); await Future.delayed(Duration(milliseconds: 100)); - inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, false); } Future onMobileVolumeUp() async => - await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF); Future onMobileVolumeDown() async => - await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF); Future onMobilePower() async => - await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage); + await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 050a92a5f64..ecbfd6fa439 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -4,15 +4,18 @@ import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/cm_file_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; @@ -267,6 +270,8 @@ class FfiModel with ChangeNotifier { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, sessionId, peerId); + } else if (name == 'toast') { + handleToast(evt, sessionId, peerId); } else if (name == 'set_multiple_windows_session') { handleMultipleWindowsSession(evt, sessionId, peerId); } else if (name == 'peer_info') { @@ -304,8 +309,13 @@ class FfiModel with ChangeNotifier { } else if (name == 'job_progress') { parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - parent.target?.fileModel.jobController.jobDone(evt); - parent.target?.fileModel.refreshAll(); + bool? refresh = + await parent.target?.fileModel.jobController.jobDone(evt); + if (refresh == true) { + // many job done for delete directory + // todo: refresh may not work when confirm delete local directory + parent.target?.fileModel.refreshAll(); + } } else if (name == 'job_error') { parent.target?.fileModel.jobController.jobError(evt); } else if (name == 'override_file_confirm') { @@ -365,7 +375,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'plugin_option') { handleOption(evt); } else if (name == "sync_peer_hash_password_to_personal_ab") { - if (desktopType == DesktopType.main) { + if (desktopType == DesktopType.main || isWeb || isMobile) { final id = evt['id']; final hash = evt['hash']; if (id != null && hash != null) { @@ -383,6 +393,14 @@ class FfiModel with ChangeNotifier { handleFollowCurrentDisplay(evt, sessionId, peerId); } else if (name == 'use_texture_render') { _handleUseTextureRender(evt, sessionId, peerId); + } else if (name == "selected_files") { + if (isWeb) { + parent.target?.fileModel.onSelectedFiles(evt); + } + } else if (name == "record_status") { + if (desktopType == DesktopType.remote || isMobile) { + parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); + } } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -492,10 +510,12 @@ class FfiModel with ChangeNotifier { newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; - newDisplay.originalWidth = - int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; - newDisplay.originalHeight = - int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; + newDisplay.originalWidth = int.tryParse( + evt['original_width'] ?? kInvalidResolutionValue.toString()) ?? + kInvalidResolutionValue; + newDisplay.originalHeight = int.tryParse( + evt['original_height'] ?? kInvalidResolutionValue.toString()) ?? + kInvalidResolutionValue; newDisplay._scale = _pi.scaleOfDisplay(display); _pi.displays[display] = newDisplay; @@ -511,7 +531,6 @@ class FfiModel with ChangeNotifier { } } - parent.target?.recordingModel.onSwitchDisplay(); if (!_pi.isSupportMultiUiSession || _pi.currentDisplay == display) { handleResolutions(peerId, evt['resolutions']); } @@ -582,13 +601,44 @@ class FfiModel with ChangeNotifier { } } + handleToast(Map evt, SessionID sessionId, String peerId) { + final type = evt['type'] ?? 'info'; + final text = evt['text'] ?? ''; + final durMsc = evt['dur_msec'] ?? 2000; + final duration = Duration(milliseconds: durMsc); + if ((text).isEmpty) { + BotToast.showLoading( + duration: duration, + clickClose: true, + allowClick: true, + ); + } else { + if (type.contains('error')) { + BotToast.showText( + contentColor: Colors.red, + text: translate(text), + duration: duration, + clickClose: true, + onlyOne: true, + ); + } else { + BotToast.showText( + text: translate(text), + duration: duration, + clickClose: true, + onlyOne: true, + ); + } + } + } + /// Show a message box with [type], [title] and [text]. showMsgBox(SessionID sessionId, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { msgBox(sessionId, type, title, text, link, dialogManager, hasCancel: hasCancel, - reconnect: reconnect, + reconnect: hasRetry ? reconnect : null, reconnectTimeout: hasRetry ? _reconnects : null); _timer?.cancel(); if (hasRetry) { @@ -788,7 +838,7 @@ class FfiModel with ChangeNotifier { isRefreshing = false; } Map features = json.decode(evt['features']); - _pi.features.privacyMode = features['privacy_mode'] == 1; + _pi.features.privacyMode = features['privacy_mode'] == true; if (!isCache) { handleResolutions(peerId, evt["resolutions"]); } @@ -832,7 +882,7 @@ class FfiModel with ChangeNotifier { for (final mode in [kKeyMapMode, kKeyLegacyMode]) { if (bind.sessionIsKeyboardModeSupported( sessionId: sessionId, mode: mode)) { - bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); + await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); break; } } @@ -1088,8 +1138,6 @@ class FfiModel with ChangeNotifier { // Directly switch to the new display without waiting for the response. switchToNewDisplay(int display, SessionID sessionId, String peerId, {bool updateCursorPos = false}) { - // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. - parent.target?.recordingModel.onClose(); // no need to wait for the response pi.currentDisplay = display; updateCurDisplay(sessionId, updateCursorPos: updateCursorPos); @@ -1178,6 +1226,27 @@ class ImageModel with ChangeNotifier { clearImage() => _image = null; + bool _webDecodingRgba = false; + final List _webRgbaList = List.empty(growable: true); + webOnRgba(int display, Uint8List rgba) async { + // deep copy needed, otherwise "instantiateCodec failed: TypeError: Cannot perform Construct on a detached ArrayBuffer" + _webRgbaList.add(Uint8List.fromList(rgba)); + if (_webDecodingRgba) { + return; + } + _webDecodingRgba = true; + try { + while (_webRgbaList.isNotEmpty) { + final rgba2 = _webRgbaList.last; + _webRgbaList.clear(); + await decodeAndUpdate(display, rgba2); + } + } catch (e) { + debugPrint('onRgba error: $e'); + } + _webDecodingRgba = false; + } + onRgba(int display, Uint8List rgba) async { try { await decodeAndUpdate(display, rgba); @@ -1590,11 +1659,25 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - clear([bool notify = false]) { + // For reset canvas to the last view style + reset() { + _scale = _lastViewStyle.scale; + _devicePixelRatio = ui.window.devicePixelRatio; + if (kIgnoreDpi && _lastViewStyle.style == kRemoteViewStyleOriginal) { + _scale = 1.0 / _devicePixelRatio; + } + final displayWidth = getDisplayWidth(); + final displayHeight = getDisplayHeight(); + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + bind.sessionSetViewStyle(sessionId: sessionId, value: _lastViewStyle.style); + notifyListeners(); + } + + clear() { _x = 0; _y = 0; _scale = 1.0; - if (notify) notifyListeners(); } updateScrollPercent() { @@ -1919,7 +2002,7 @@ class CursorModel with ChangeNotifier { _x = _displayOriginX; _y = _displayOriginY; parent.target?.inputModel.moveMouse(_x, _y); - parent.target?.canvasModel.clear(true); + parent.target?.canvasModel.reset(); notifyListeners(); } @@ -2178,6 +2261,7 @@ class CursorModel with ChangeNotifier { debugPrint("deleting cursor with key $k"); deleteCustomCursor(k); } + resetSystemCursor(); } trySetRemoteWindowCoords() { @@ -2224,8 +2308,10 @@ class QualityMonitorModel with ChangeNotifier { updateQualityStatus(Map evt) { try { - if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; - if ((evt['fps'] as String).isNotEmpty) { + if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) { + _data.speed = evt['speed']; + } + if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) { final fps = jsonDecode(evt['fps']) as Map; final pi = parent.target?.ffiModel.pi; if (pi != null) { @@ -2246,14 +2332,18 @@ class QualityMonitorModel with ChangeNotifier { _data.fps = null; } } - if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; - if ((evt['target_bitrate'] as String).isNotEmpty) { + if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) { + _data.delay = evt['delay']; + } + if (evt.containsKey('target_bitrate') && + (evt['target_bitrate'] as String).isNotEmpty) { _data.targetBitrate = evt['target_bitrate']; } - if ((evt['codec_format'] as String).isNotEmpty) { + if (evt.containsKey('codec_format') && + (evt['codec_format'] as String).isNotEmpty) { _data.codecFormat = evt['codec_format']; } - if ((evt['chroma'] as String).isNotEmpty) { + if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) { _data.chroma = evt['chroma']; } notifyListeners(); @@ -2267,25 +2357,7 @@ class RecordingModel with ChangeNotifier { WeakReference parent; RecordingModel(this.parent); bool _start = false; - get start => _start; - - onSwitchDisplay() { - if (isIOS || !_start) return; - final sessionId = parent.target?.sessionId; - int? width = parent.target?.canvasModel.getDisplayWidth(); - int? height = parent.target?.canvasModel.getDisplayHeight(); - if (sessionId == null || width == null || height == null) return; - final pi = parent.target?.ffiModel.pi; - if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - bind.sessionRecordScreen( - sessionId: sessionId, - start: true, - display: currentDisplay, - width: width, - height: height); - } + bool get start => _start; toggle() async { if (isIOS) return; @@ -2293,48 +2365,16 @@ class RecordingModel with ChangeNotifier { if (sessionId == null) return; final pi = parent.target?.ffiModel.pi; if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - _start = !_start; - notifyListeners(); - await _sendStatusMessage(sessionId, pi, _start); - if (_start) { - sessionRefreshVideo(sessionId, pi); - if (versionCmp(pi.version, '1.2.4') >= 0) { - // will not receive SwitchDisplay since 1.2.4 - onSwitchDisplay(); - } - } else { - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay, - width: 0, - height: 0); + bool value = !_start; + if (value) { + await sessionRefreshVideo(sessionId, pi); } + await bind.sessionRecordScreen(sessionId: sessionId, start: value); } - onClose() async { - if (isIOS) return; - final sessionId = parent.target?.sessionId; - if (sessionId == null) return; - if (!_start) return; - _start = false; - final pi = parent.target?.ffiModel.pi; - if (pi == null) return; - final currentDisplay = pi.currentDisplay; - if (currentDisplay == kAllDisplayValue) return; - await _sendStatusMessage(sessionId, pi, false); - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay, - width: 0, - height: 0); - } - - _sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async { - await bind.sessionRecordStatus(sessionId: sessionId, status: status); + updateStatus(bool status) { + _start = status; + notifyListeners(); } } @@ -2383,6 +2423,9 @@ class FFI { late final ElevationModel elevationModel; // session late final CmFileModel cmFileModel; // cm late final TextureModel textureModel; //session + late final Peers recentPeersModel; // global + late final Peers favoritePeersModel; // global + late final Peers lanPeersModel; // global FFI(SessionID? sId) { sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); @@ -2403,6 +2446,16 @@ class FFI { elevationModel = ElevationModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this)); + recentPeersModel = Peers( + name: PeersModelName.recent, + loadEvent: LoadEvent.recent, + getInitPeers: null); + favoritePeersModel = Peers( + name: PeersModelName.favorite, + loadEvent: LoadEvent.favorite, + getInitPeers: null); + lanPeersModel = Peers( + name: PeersModelName.lan, loadEvent: LoadEvent.lan, getInitPeers: null); } /// Mobile reuse FFI @@ -2423,6 +2476,7 @@ class FFI { String? switchUuid, String? password, bool? isSharedPassword, + String? connToken, bool? forceRelay, int? tabWindowId, int? display, @@ -2459,6 +2513,7 @@ class FFI { forceRelay: forceRelay ?? false, password: password ?? '', isSharedPassword: isSharedPassword ?? false, + connToken: connToken, ); } else if (display != null) { if (displays == null) { @@ -2497,6 +2552,7 @@ class FFI { onEvent2UIRgba(); imageModel.onRgba(display, data); }); + this.id = id; return; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index b99cf2e7fb8..c8d5085e897 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -48,6 +48,12 @@ class PlatformFFI { static get isMain => instance._appType == kAppTypeMain; + static String getByName(String name, [String arg = '']) { + return ''; + } + + static void setByName(String name, [String value = '']) {} + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -276,4 +282,6 @@ class PlatformFFI { void syncAndroidServiceAppDirConfigPath() { invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); } + + void setFullscreenCallback(void Function(bool) fun) {} } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 188dd4e0bdf..7ab5a2b803e 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -194,10 +194,14 @@ class Peers extends ChangeNotifier { } void _updateOnlineState(Map evt) { + int changedCount = 0; evt['onlines'].split(',').forEach((online) { for (var i = 0; i < peers.length; i++) { if (peers[i].id == online) { - peers[i].online = true; + if (!peers[i].online) { + changedCount += 1; + peers[i].online = true; + } } } }); @@ -205,13 +209,18 @@ class Peers extends ChangeNotifier { evt['offlines'].split(',').forEach((offline) { for (var i = 0; i < peers.length; i++) { if (peers[i].id == offline) { - peers[i].online = false; + if (peers[i].online) { + changedCount += 1; + peers[i].online = false; + } } } }); - event = UpdateEvent.online; - notifyListeners(); + if (changedCount > 0) { + event = UpdateEvent.online; + notifyListeners(); + } } void _updatePeers(Map evt) { diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 3c0fe636d68..83df1f05d6a 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -152,7 +152,7 @@ class PeerTabModel with ChangeNotifier { // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700 // After onTap, the shift key should be pressed for a while when not in multiselection mode, // because onTap is delayed when onDoubleTap is not null - if (isDesktop && !_isShiftDown) return; + if (isDesktop || isWebDesktop) return; _multiSelectionMode = true; } final cached = _currentTabCachedPeers.map((e) => e.id).toList(); @@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier { notifyListeners(); } + // `notifyListeners()` will cause many rebuilds. + // So, we need to reduce the calls to "notifyListeners()" only when necessary. + // A better way is to use a new model. setCurrentTabCachedPeers(List peers) { Future.delayed(Duration.zero, () { + final isPreEmpty = _currentTabCachedPeers.isEmpty; _currentTabCachedPeers = peers; - notifyListeners(); + final isNowEmpty = _currentTabCachedPeers.isEmpty; + if (isPreEmpty != isNowEmpty) { + notifyListeners(); + } }); } diff --git a/flutter/lib/models/platform_model.dart b/flutter/lib/models/platform_model.dart index 6bc770ff666..0f21587ad8d 100644 --- a/flutter/lib/models/platform_model.dart +++ b/flutter/lib/models/platform_model.dart @@ -6,3 +6,11 @@ final platformFFI = PlatformFFI.instance; final localeName = PlatformFFI.localeName; RustdeskImpl get bind => platformFFI.ffiBind; + +String ffiGetByName(String name, [String arg = '']) { + return PlatformFFI.getByName(name, arg); +} + +void ffiSetByName(String name, [String value = '']) { + PlatformFFI.setByName(name, value); +} diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 613fee6ad11..1d800ef6967 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -826,7 +826,7 @@ class Client { Map toJson() { final Map data = {}; data['id'] = id; - data['is_start'] = authorized; + data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; data['port_forward'] = portForward; data['name'] = name; @@ -840,6 +840,8 @@ class Client { data['block_input'] = blockInput; data['disconnected'] = disconnected; data['from_switch'] = fromSwitch; + data['in_voice_call'] = inVoiceCall; + data['incoming_voice_call'] = incomingVoiceCall; return data; } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 7c4d3cfd059..f8f06cc3fb8 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -19,6 +19,11 @@ class StateGlobal { final RxBool showRemoteToolBar = false.obs; final svcStatus = SvcStatus.notReady.obs; final RxBool isFocused = false.obs; + // for mobile and web + bool isInMainPage = true; + bool isWebVisible = true; + + final isPortrait = false.obs; String _inputSource = ''; @@ -68,27 +73,40 @@ class StateGlobal { if (_fullscreen.value != v) { _fullscreen.value = v; _showTabBar.value = !_fullscreen.value; - refreshResizeEdgeSize(); - print( - "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); - _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; - if (procWnd) { - final wc = WindowController.fromWindowId(windowId); - wc.setFullscreen(_fullscreen.isTrue).then((_) { - // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 - if (isWindows && !v) { - Future.delayed(Duration.zero, () async { - final frame = await wc.getFrame(); - final newRect = Rect.fromLTWH( - frame.left, frame.top, frame.width + 1, frame.height + 1); - await wc.setFrame(newRect); - }); - } - }); + if (isWebDesktop) { + procFullscreenWeb(); + } else { + procFullscreenNative(procWnd); } } } + procFullscreenWeb() { + final isFullscreen = ffiGetByName('fullscreen') == 'Y'; + String fullscreenValue = ''; + if (isFullscreen && _fullscreen.isFalse) { + fullscreenValue = 'N'; + } else if (!isFullscreen && fullscreen.isTrue) { + fullscreenValue = 'Y'; + } + if (fullscreenValue.isNotEmpty) { + ffiSetByName('fullscreen', fullscreenValue); + } + } + + procFullscreenNative(bool procWnd) { + refreshResizeEdgeSize(); + print("fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); + _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; + if (procWnd) { + final wc = WindowController.fromWindowId(windowId); + wc.setFullscreen(_fullscreen.isTrue).then((_) { + // We remove the redraw (width + 1, height + 1), because this issue cannot be reproduced. + // https://github.com/rustdesk/rustdesk/issues/9675 + }); + } + } + refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue ? kFullScreenEdgeSize : isMaximized.isTrue @@ -107,7 +125,13 @@ class StateGlobal { _inputSource = bind.mainGetInputSource(); } - StateGlobal._(); + StateGlobal._() { + if (isWebDesktop) { + platformFFI.setFullscreenCallback((v) { + _fullscreen.value = v; + }); + } + } static final StateGlobal instance = StateGlobal._(); } diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 4896781a9cc..a4312d959c7 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -1,12 +1,14 @@ // ignore_for_file: avoid_web_libraries_in_flutter import 'dart:convert'; +import 'dart:js_interop'; import 'dart:typed_data'; import 'dart:js'; import 'dart:html'; import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/web/bridge.dart'; import 'package:flutter_hbb/common.dart'; @@ -28,7 +30,15 @@ class PlatformFFI { context.callMethod('setByName', [name, value]); } - PlatformFFI._(); + PlatformFFI._() { + window.document.addEventListener( + 'visibilitychange', + (event) => { + stateGlobal.isWebVisible = + window.document.visibilityState == 'visible' + }); + } + static final PlatformFFI instance = PlatformFFI._(); static get localeName => window.navigator.language; @@ -98,6 +108,10 @@ class PlatformFFI { sessionId: sessionId, display: display, ptr: ptr); Future init(String appType) async { + Completer completer = Completer(); + context["onInitFinished"] = () { + completer.complete(); + }; context.callMethod('init'); version = getByName('version'); window.onContextMenu.listen((event) { @@ -112,6 +126,7 @@ class PlatformFFI { print('json.decode fail(): $e'); } }; + return completer.future; } void setEventCallback(void Function(Map) fun) { @@ -157,4 +172,10 @@ class PlatformFFI { // just for compilation void syncAndroidServiceAppDirConfigPath() {} + + void setFullscreenCallback(void Function(bool) fun) { + context["onFullscreenChanged"] = (bool v) { + fun(v); + }; + } } diff --git a/flutter/lib/native/common.dart b/flutter/lib/native/common.dart index d3888a245d1..96d5bd6e82a 100644 --- a/flutter/lib/native/common.dart +++ b/flutter/lib/native/common.dart @@ -11,3 +11,7 @@ final isWebDesktop_ = false; final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux; String get screenInfo_ => ''; + +final isWebOnWindows_ = false; +final isWebOnLinux_ = false; +final isWebOnMacOS_ = false; diff --git a/flutter/lib/native/custom_cursor.dart b/flutter/lib/native/custom_cursor.dart index 3e53f3cc5b2..e85d42a5589 100644 --- a/flutter/lib/native/custom_cursor.dart +++ b/flutter/lib/native/custom_cursor.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart'; deleteCustomCursor(String key) => custom_cursor_manager.CursorManager.instance.deleteCursor(key); +resetSystemCursor() {} MouseCursor buildCursorOfCache( CursorModel cursor, double scale, CursorData? cache) { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 191152c8625..70001ffdff4 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -124,6 +124,9 @@ class RustDeskMultiWindowManager { bool withScreenRect, ) async { final windowController = await DesktopMultiWindow.createWindow(msg); + if (isWindows) { + windowController.setInitBackgroundColor(Colors.black); + } final windowId = windowController.windowId; if (!withScreenRect) { windowController @@ -198,6 +201,7 @@ class RustDeskMultiWindowManager { String? switchUuid, bool? isRDP, bool? isSharedPassword, + String? connToken, }) async { var params = { "type": type.index, @@ -214,6 +218,9 @@ class RustDeskMultiWindowManager { if (isSharedPassword != null) { params['isSharedPassword'] = isSharedPassword; } + if (connToken != null) { + params['connToken'] = connToken; + } final msg = jsonEncode(params); // separate window for file transfer is not supported @@ -251,8 +258,13 @@ class RustDeskMultiWindowManager { ); } - Future newFileTransfer(String remoteId, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newFileTransfer( + String remoteId, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.FileTransfer, kWindowEventNewFileTransfer, @@ -261,11 +273,18 @@ class RustDeskMultiWindowManager { password: password, forceRelay: forceRelay, isSharedPassword: isSharedPassword, + connToken: connToken, ); } - Future newPortForward(String remoteId, bool isRDP, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newPortForward( + String remoteId, + bool isRDP, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.PortForward, kWindowEventNewPortForward, @@ -275,6 +294,7 @@ class RustDeskMultiWindowManager { forceRelay: forceRelay, isRDP: isRDP, isSharedPassword: isSharedPassword, + connToken: connToken, ); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 457911458ce..20891281455 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; +import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; @@ -23,6 +24,7 @@ sealed class EventToUI { ) = EventToUI_Rgba; const factory EventToUI.texture( int field0, + bool field1, ) = EventToUI_Texture; } @@ -33,25 +35,29 @@ class EventToUI_Event implements EventToUI { } class EventToUI_Rgba implements EventToUI { - const EventToUI_Rgba(final int field0) : this.field = field0; + const EventToUI_Rgba(final int field0) : field = field0; final int field; int get field0 => field; } class EventToUI_Texture implements EventToUI { - const EventToUI_Texture(final int field0) : this.field = field0; - final int field; - int get field0 => field; + const EventToUI_Texture(final int field0, final bool field1) + : f0 = field0, + f1 = field1; + final int f0; + final bool f1; + int get field0 => f0; + bool get field1 => f1; } class RustdeskImpl { Future stopGlobalEventStream({required String appType, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("stopGlobalEventStream"); } Future hostStopSystemKeyPropagate( {required bool stopped, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("hostStopSystemKeyPropagate"); } int peerGetDefaultSessionsCount({required String id, dynamic hint}) { @@ -76,10 +82,16 @@ class RustdeskImpl { required bool forceRelay, required String password, required bool isSharedPassword, + String? connToken, dynamic hint}) { return js.context.callMethod('setByName', [ 'session_add_sync', - jsonEncode({'id': id, 'password': password}) + jsonEncode({ + 'id': id, + 'password': password, + 'is_shared_password': isSharedPassword, + 'isFileTransfer': isFileTransfer + }) ]); } @@ -97,7 +109,7 @@ class RustdeskImpl { required String id, required Int32List displays, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionStartWithDisplays"); } Future sessionGetRemember( @@ -146,7 +158,10 @@ class RustdeskImpl { required String code, required bool trustThisDevice, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['send_2fa', code])); + return Future(() => js.context.callMethod('setByName', [ + 'send_2fa', + jsonEncode({'code': code, 'trust_this_device': trustThisDevice}) + ])); } Future sessionClose({required UuidValue sessionId, dynamic hint}) { @@ -159,18 +174,12 @@ class RustdeskImpl { } Future sessionRecordScreen( - {required UuidValue sessionId, - required bool start, - required int display, - required int width, - required int height, - dynamic hint}) { - throw UnimplementedError(); + {required UuidValue sessionId, required bool start, dynamic hint}) { + throw UnimplementedError("sessionRecordScreen"); } - Future sessionRecordStatus( - {required UuidValue sessionId, required bool status, dynamic hint}) { - throw UnimplementedError(); + bool sessionGetIsRecording({required UuidValue sessionId, dynamic hint}) { + return false; } Future sessionReconnect( @@ -181,7 +190,7 @@ class RustdeskImpl { Future sessionToggleOption( {required UuidValue sessionId, required String value, dynamic hint}) { return Future( - () => js.context.callMethod('setByName', ['toggle_option', value])); + () => js.context.callMethod('setByName', ['option:toggle', value])); } Future sessionTogglePrivacyMode( @@ -190,8 +199,8 @@ class RustdeskImpl { required bool on, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'toggle_option', - jsonEncode({implKey, on}) + 'toggle_privacy_mode', + jsonEncode({'impl_key': implKey, 'on': on}) ])); } @@ -229,7 +238,7 @@ class RustdeskImpl { } String getLocalKbLayoutType({dynamic hint}) { - throw js.context.callMethod('getByName', ['option:local', 'kb_layout']); + return js.context.callMethod('getByName', ['option:local', 'kb_layout']); } Future setLocalKbLayoutType( @@ -270,16 +279,14 @@ class RustdeskImpl { Future sessionGetImageQuality( {required UuidValue sessionId, dynamic hint}) { - return Future(() => js.context - .callMethod('getByName', ['option:session', 'image_quality'])); + return Future(() => js.context.callMethod('getByName', ['image_quality'])); } Future sessionSetImageQuality( {required UuidValue sessionId, required String value, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'image_quality', 'value': value}) - ])); + print('set image quality: $value'); + return Future( + () => js.context.callMethod('setByName', ['image_quality', value])); } Future sessionGetKeyboardMode( @@ -346,7 +353,11 @@ class RustdeskImpl { bool sessionIsKeyboardModeSupported( {required UuidValue sessionId, required String mode, dynamic hint}) { - return mode == kKeyLegacyMode; + if (mainGetInputSource(hint: hint) == 'Input source 1') { + return [kKeyMapMode, kKeyTranslateMode].contains(mode); + } else { + return [kKeyLegacyMode, kKeyMapMode].contains(mode); + } } bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) { @@ -356,17 +367,15 @@ class RustdeskImpl { Future sessionSetCustomImageQuality( {required UuidValue sessionId, required int value, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'custom_image_quality', 'value': value}) + 'custom_image_quality', + value, ])); } Future sessionSetCustomFps( {required UuidValue sessionId, required int fps, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ - 'option:session', - jsonEncode({'name': 'custom_fps', 'value': fps}) - ])); + return Future( + () => js.context.callMethod('setByName', ['custom-fps', fps])); } Future sessionLockScreen({required UuidValue sessionId, dynamic hint}) { @@ -385,14 +394,32 @@ class RustdeskImpl { return Future(() => js.context.callMethod('setByName', [ 'switch_display', jsonEncode({ - isDesktop: isDesktop, - sessionId: sessionId.toString(), - value: value + 'isDesktop': isDesktop, + 'sessionId': sessionId.toString(), + 'value': value }) ])); } Future sessionHandleFlutterKeyEvent( + {required UuidValue sessionId, + required String character, + required int usbHid, + required int lockModes, + required bool downOrUp, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'flutter_key_event', + jsonEncode({ + 'name': character, + 'usb_hid': usbHid, + 'lock_modes': lockModes, + if (downOrUp) 'down': 'true', + }) + ])); + } + + Future sessionHandleFlutterRawKeyEvent( {required UuidValue sessionId, required String name, required int platformCode, @@ -400,13 +427,12 @@ class RustdeskImpl { required int lockModes, required bool downOrUp, dynamic hint}) { - // TODO: map mode - throw UnimplementedError(); + throw UnimplementedError("sessionHandleFlutterRawKeyEvent"); } void sessionEnterOrLeave( {required UuidValue sessionId, required bool enter, dynamic hint}) { - throw UnimplementedError(); + js.context.callMethod('setByName', ['enter_or_leave', enter]); } Future sessionInputKey( @@ -441,7 +467,8 @@ class RustdeskImpl { Future sessionSendChat( {required UuidValue sessionId, required String text, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['send_chat', text])); } Future sessionPeerOption( @@ -449,7 +476,7 @@ class RustdeskImpl { required String name, required String value, dynamic hint}) { - return Future(() => js.context.callMethod('SetByName', [ + return Future(() => js.context.callMethod('setByName', [ 'option:session', jsonEncode({'name': name, 'value': value}) ])); @@ -472,7 +499,10 @@ class RustdeskImpl { required String path, required bool includeHidden, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'read_remote_dir', + jsonEncode({'path': path, 'include_hidden': includeHidden}) + ])); } Future sessionSendFiles( @@ -483,8 +513,20 @@ class RustdeskImpl { required int fileNum, required bool includeHidden, required bool isRemote, + required bool isDir, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'send_files', + jsonEncode({ + 'id': actId, + 'path': path, + 'to': to, + 'file_num': fileNum, + 'include_hidden': includeHidden, + 'is_remote': isRemote, + 'is_dir': isDir, + }) + ])); } Future sessionSetConfirmOverrideFile( @@ -495,7 +537,16 @@ class RustdeskImpl { required bool remember, required bool isUpload, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'confirm_override_file', + jsonEncode({ + 'id': actId, + 'file_num': fileNum, + 'need_override': needOverride, + 'remember': remember, + 'is_upload': isUpload + }) + ])); } Future sessionRemoveFile( @@ -505,17 +556,33 @@ class RustdeskImpl { required int fileNum, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'remove_file', + jsonEncode({ + 'id': actId, + 'path': path, + 'file_num': fileNum, + 'is_remote': isRemote + }) + ])); } - Future sessionReadDirRecursive( + Future sessionReadDirToRemoveRecursive( {required UuidValue sessionId, required int actId, required String path, required bool isRemote, required bool showHidden, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'read_dir_to_remove_recursive', + jsonEncode({ + 'id': actId, + 'path': path, + 'is_remote': isRemote, + 'show_hidden': showHidden + }) + ])); } Future sessionRemoveAllEmptyDirs( @@ -524,12 +591,16 @@ class RustdeskImpl { required String path, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'remove_all_empty_dirs', + jsonEncode({'id': actId, 'path': path, 'is_remote': isRemote}) + ])); } Future sessionCancelJob( {required UuidValue sessionId, required int actId, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['cancel_job', actId])); } Future sessionCreateDir( @@ -538,7 +609,10 @@ class RustdeskImpl { required String path, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'create_dir', + jsonEncode({'id': actId, 'path': path, 'is_remote': isRemote}) + ])); } Future sessionReadLocalDirSync( @@ -546,17 +620,21 @@ class RustdeskImpl { required String path, required bool showHidden, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionReadLocalDirSync"); } Future sessionGetPlatform( {required UuidValue sessionId, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + if (isRemote) { + return Future(() => js.context.callMethod('getByName', ['platform'])); + } else { + return Future(() => 'Web'); + } } Future sessionLoadLastTransferJobs( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionLoadLastTransferJobs"); } Future sessionAddJob( @@ -568,7 +646,7 @@ class RustdeskImpl { required bool includeHidden, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionAddJob"); } Future sessionResumeJob( @@ -576,12 +654,12 @@ class RustdeskImpl { required int actId, required bool isRemote, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionResumeJob"); } Future sessionElevateDirect( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['elevate_direct'])); } Future sessionElevateWithLogon( @@ -591,13 +669,13 @@ class RustdeskImpl { dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ 'elevate_with_logon', - jsonEncode({username, password}) + jsonEncode({'username': username, 'password': password}) ])); } Future sessionSwitchSides( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSwitchSides"); } Future sessionChangeResolution( @@ -607,7 +685,10 @@ class RustdeskImpl { required int height, dynamic hint}) { // note: restore on disconnected - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'change_resolution', + jsonEncode({'display': display, 'width': width, 'height': height}) + ])); } Future sessionSetSize( @@ -621,27 +702,36 @@ class RustdeskImpl { Future sessionSendSelectedSessionId( {required UuidValue sessionId, required String sid, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['selected_sid', sid])); } Future> mainGetSoundInputs({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSoundInputs"); } Future mainGetDefaultSoundInput({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDefaultSoundInput"); } String mainGetLoginDeviceInfo({dynamic hint}) { - throw UnimplementedError(); + String userAgent = html.window.navigator.userAgent; + String appName = html.window.navigator.appName; + String appVersion = html.window.navigator.appVersion; + String? platform = html.window.navigator.platform; + return jsonEncode({ + 'os': '$userAgent, $appName $appVersion ($platform)', + 'type': 'Web client', + 'name': js.context.callMethod('getByName', ['my_name']), + }); } Future mainChangeId({required String newId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeId"); } Future mainGetAsyncStatus({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetAsyncStatus"); } Future mainGetOption({required String key, dynamic hint}) { @@ -653,11 +743,11 @@ class RustdeskImpl { } Future mainGetError({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetError"); } bool mainShowOption({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainShowOption"); } Future mainSetOption( @@ -694,27 +784,28 @@ class RustdeskImpl { required String username, required String password, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetSocks"); } Future> mainGetSocks({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetSocks"); } Future mainGetAppName({dynamic hint}) { - throw UnimplementedError(); + return Future.value(mainGetAppNameSync(hint: hint)); } String mainGetAppNameSync({dynamic hint}) { - throw UnimplementedError(); + return 'RustDesk'; } String mainUriPrefixSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUriPrefixSync"); } Future mainGetLicense({dynamic hint}) { - throw UnimplementedError(); + // TODO: implement + return Future(() => ''); } Future mainGetVersion({dynamic hint}) { @@ -741,11 +832,11 @@ class RustdeskImpl { String mainGetPeerSync({required String id, dynamic hint}) { // TODO: - throw UnimplementedError(); + throw UnimplementedError("mainGetPeerSync"); } Future mainGetLanPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetLanPeers"); } Future mainGetConnectStatus({dynamic hint}) { @@ -754,16 +845,17 @@ class RustdeskImpl { } Future mainCheckConnectStatus({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckConnectStatus"); } Future mainIsUsingPublicServer({dynamic hint}) { - return Future( - () => js.context.callMethod('setByName', ["is_using_public_server"])); + return Future(() => + js.context.callMethod('getByName', ["is_using_public_server"]) == + 'true'); } Future mainDiscover({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainDiscover"); } Future mainGetApiServer({dynamic hint}) { @@ -775,7 +867,7 @@ class RustdeskImpl { required String body, required String header, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainPostRequest"); } Future mainGetProxyStatus({dynamic hint}) { @@ -789,11 +881,11 @@ class RustdeskImpl { required String header, dynamic hint, }) { - throw UnimplementedError(); + throw UnimplementedError("mainHttpRequest"); } Future mainGetHttpStatus({required String url, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetHttpStatus"); } String mainGetLocalOption({required String key, dynamic hint}) { @@ -801,7 +893,7 @@ class RustdeskImpl { } String mainGetEnv({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetEnv"); } Future mainSetLocalOption( @@ -813,24 +905,29 @@ class RustdeskImpl { } String mainGetInputSource({dynamic hint}) { - // // rdev grab mode - // const CONFIG_INPUT_SOURCE_1 = "Input source 1"; + final inputSource = + js.context.callMethod('getByName', ['option:local', 'input-source']); + // // js grab mode + // export const CONFIG_INPUT_SOURCE_1 = "Input source 1"; // // flutter grab mode - // const CONFIG_INPUT_SOURCE_2 = "Input source 2"; - return 'Input source 2'; + // export const CONFIG_INPUT_SOURCE_2 = "Input source 2"; + return inputSource != '' ? inputSource : 'Input source 1'; } Future mainSetInputSource( {required UuidValue sessionId, required String value, dynamic hint}) { - return Future.value(); + return Future(() => js.context.callMethod('setByName', [ + 'option:local', + jsonEncode({'name': 'input-source', 'value': value}) + ])); } Future mainGetMyId({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['my_id'])); } Future mainGetUuid({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['uuid'])); } Future mainGetPeerOption( @@ -890,11 +987,11 @@ class RustdeskImpl { } Future mainGetNewStoredPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetNewStoredPeers"); } Future mainForgetPassword({required String id, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['forget'])); + return mainSetPeerOption(id: id, key: 'password', value: ''); } Future mainPeerHasPassword({required String id, dynamic hint}) { @@ -923,7 +1020,7 @@ class RustdeskImpl { Future mainLoadRecentPeersForAb( {required String filter, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainLoadRecentPeersForAb"); } Future mainLoadFavPeers({dynamic hint}) { @@ -931,31 +1028,32 @@ class RustdeskImpl { } Future mainLoadLanPeers({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainLoadLanPeers"); } Future mainRemoveDiscovered({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainRemoveDiscovered"); } Future mainChangeTheme({required String dark, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeTheme"); } Future mainChangeLanguage({required String lang, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainChangeLanguage"); } String mainVideoSaveDirectory({required bool root, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVideoSaveDirectory"); } Future mainSetUserDefaultOption( {required String key, required String value, dynamic hint}) { - return js.context.callMethod('getByName', [ + js.context.callMethod('setByName', [ 'option:user:default', jsonEncode({'name': key, 'value': value}) ]); + return Future.value(); } String mainGetUserDefaultOption({required String key, dynamic hint}) { @@ -975,7 +1073,7 @@ class RustdeskImpl { } String mainGetDisplays({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDisplays"); } Future sessionAddPortForward( @@ -984,44 +1082,43 @@ class RustdeskImpl { required String remoteHost, required int remotePort, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionAddPortForward"); } Future sessionRemovePortForward( {required UuidValue sessionId, required int localPort, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRemovePortForward"); } Future sessionNewRdp({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionNewRdp"); } Future sessionRequestVoiceCall( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRequestVoiceCall"); } Future sessionCloseVoiceCall( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionCloseVoiceCall"); } Future cmHandleIncomingVoiceCall( {required int id, required bool accept, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmHandleIncomingVoiceCall"); } Future cmCloseVoiceCall({required int id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCloseVoiceCall"); } Future mainGetLastRemoteId({dynamic hint}) { - return Future( - () => js.context.callMethod('getByName', ['option', 'last_remote_id'])); + return Future(() => mainGetLocalOption(key: 'last_remote_id')); } - Future mainGetSoftwareUpdateUrl({dynamic hint}) { - throw UnimplementedError(); + Future mainGetSoftwareUpdateUrl({dynamic hint}) { + throw UnimplementedError("mainGetSoftwareUpdateUrl"); } Future mainGetHomeDir({dynamic hint}) { @@ -1029,7 +1126,7 @@ class RustdeskImpl { } Future mainGetLangs({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['langs'])); } Future mainGetTemporaryPassword({dynamic hint}) { @@ -1041,19 +1138,19 @@ class RustdeskImpl { } Future mainGetFingerprint({dynamic hint}) { - throw UnimplementedError(); + return Future.value(''); } Future cmGetClientsState({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetClientsState"); } Future cmCheckClientsLength({required int length, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClientsLength"); } Future cmGetClientsLength({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClientsLength"); } Future mainInit({required String appDir, dynamic hint}) { @@ -1062,69 +1159,95 @@ class RustdeskImpl { Future mainDeviceId({required String id, dynamic hint}) { // TODO: ? - throw UnimplementedError(); + throw UnimplementedError("mainDeviceId"); } Future mainDeviceName({required String name, dynamic hint}) { // TODO: ? - throw UnimplementedError(); + throw UnimplementedError("mainDeviceName"); } Future mainRemovePeer({required String id, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', ['remove', id])); + return Future( + () => js.context.callMethod('setByName', ['remove_peer', id])); } bool mainHasHwcodec({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasHwcodec"); } bool mainHasVram({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasVram"); } String mainSupportedHwdecodings({dynamic hint}) { - throw UnimplementedError(); + return '{}'; } Future mainIsRoot({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsRoot"); } int getDoubleClickTime({dynamic hint}) { - throw UnimplementedError(); + return 500; } Future mainStartDbusServer({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartDbusServer"); } Future mainSaveAb({required String json, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['save_ab', json])); } Future mainClearAb({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['clear_ab'])); } Future mainLoadAb({dynamic hint}) { - throw UnimplementedError(); + Completer completer = Completer(); + Future timeoutFuture = completer.future.timeout( + Duration(seconds: 2), + onTimeout: () { + completer.completeError(TimeoutException('Load ab timed out')); + return 'Timeout'; + }, + ); + js.context["onLoadAbFinished"] = (String s) { + completer.complete(s); + }; + js.context.callMethod('setByName', ['load_ab']); + return timeoutFuture; } Future mainSaveGroup({required String json, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['save_group', json])); } Future mainClearGroup({dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['clear_group'])); } Future mainLoadGroup({dynamic hint}) { - throw UnimplementedError(); + Completer completer = Completer(); + Future timeoutFuture = completer.future.timeout( + Duration(seconds: 2), + onTimeout: () { + completer.completeError(TimeoutException('Load group timed out')); + return 'Timeout'; + }, + ); + js.context["onLoadGroupFinished"] = (String s) { + completer.complete(s); + }; + js.context.callMethod('setByName', ['load_group']); + return timeoutFuture; } Future sessionSendPointer( {required UuidValue sessionId, required String msg, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionSendPointer"); } Future sessionSendMouse( @@ -1145,7 +1268,8 @@ class RustdeskImpl { Future sessionSendNote( {required UuidValue sessionId, required String note, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['send_note', note])); } Future sessionAlternativeCodecs( @@ -1170,81 +1294,82 @@ class RustdeskImpl { required int index, required bool on, dynamic hint}) { - // TODO - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'toggle_virtual_display', + jsonEncode({'index': index, 'on': on}) + ])); } Future mainSetHomeDir({required String home, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetHomeDir"); } String mainGetDataDirIos({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetDataDirIos"); } Future mainStopService({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStopService"); } Future mainStartService({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartService"); } Future mainUpdateTemporaryPassword({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUpdateTemporaryPassword"); } Future mainSetPermanentPassword( {required String password, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetPermanentPassword"); } Future mainCheckSuperUserPermission({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckSuperUserPermission"); } Future mainCheckMouseTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckMouseTime"); } Future mainGetMouseTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetMouseTime"); } Future mainWol({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainWol"); } Future mainCreateShortcut({required String id, dynamic hint}) { - // TODO: - throw UnimplementedError(); + throw UnimplementedError("mainCreateShortcut"); } Future cmSendChat( {required int connId, required String msg, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSendChat"); } Future cmLoginRes( {required int connId, required bool res, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmLoginRes"); } Future cmCloseConnection({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCloseConnection"); } Future cmRemoveDisconnectedConnection( {required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmRemoveDisconnectedConnection"); } Future cmCheckClickTime({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCheckClickTime"); } Future cmGetClickTime({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetClickTime"); } Future cmSwitchPermission( @@ -1252,28 +1377,27 @@ class RustdeskImpl { required String name, required bool enabled, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSwitchPermission"); } bool cmCanElevate({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmCanElevate"); } Future cmElevatePortable({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmElevatePortable"); } Future cmSwitchBack({required int connId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmSwitchBack"); } Future cmGetConfig({required String name, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmGetConfig"); } Future mainGetBuildDate({dynamic hint}) { - // TODO - throw UnimplementedError(); + return Future(() => js.context.callMethod('getByName', ['build_date'])); } String translate( @@ -1322,89 +1446,89 @@ class RustdeskImpl { } bool mainIsInstalled({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalled"); } void mainInitInputSource({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalled"); } bool mainIsInstalledLowerVersion({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalledLowerVersion"); } bool mainIsInstalledDaemon({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsInstalledDaemon"); } bool mainIsProcessTrusted({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsProcessTrusted"); } bool mainIsCanScreenRecording({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsCanScreenRecording"); } bool mainIsCanInputMonitoring({required bool prompt, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsCanInputMonitoring"); } bool mainIsShareRdp({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainIsShareRdp"); } Future mainSetShareRdp({required bool enable, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetShareRdp"); } bool mainGotoInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGotoInstall"); } String mainGetNewVersion({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetNewVersion"); } bool mainUpdateMe({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainUpdateMe"); } Future setCurSessionId({required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("setCurSessionId"); } bool installShowRunWithoutInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installShowRunWithoutInstall"); } Future installRunWithoutInstall({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installRunWithoutInstall"); } Future installInstallMe( {required String options, required String path, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallMe"); } String installInstallPath({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("installInstallPath"); } Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuth"); } Future mainAccountAuthCancel({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuthCancel"); } Future mainAccountAuthResult({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainAccountAuthResult"); } Future mainOnMainWindowClose({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainOnMainWindowClose"); } bool mainCurrentIsWayland({dynamic hint}) { @@ -1416,7 +1540,7 @@ class RustdeskImpl { } bool mainHideDock({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHideDock"); } bool mainHasFileClipboard({dynamic hint}) { @@ -1428,11 +1552,11 @@ class RustdeskImpl { } Future cmInit({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("cmInit"); } Future mainStartIpcUrlServer({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainStartIpcUrlServer"); } Future mainTestWallpaper({required int second, dynamic hint}) { @@ -1482,7 +1606,7 @@ class RustdeskImpl { } Future sendUrlScheme({required String url, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sendUrlScheme"); } Future pluginEvent( @@ -1490,12 +1614,12 @@ class RustdeskImpl { required String peer, required Uint8List event, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginEvent"); } Stream pluginRegisterEventStream( {required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginRegisterEventStream"); } String? pluginGetSessionOption( @@ -1503,7 +1627,7 @@ class RustdeskImpl { required String peer, required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginGetSessionOption"); } Future pluginSetSessionOption( @@ -1512,12 +1636,12 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSetSessionOption"); } String? pluginGetSharedOption( {required String id, required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginGetSharedOption"); } Future pluginSetSharedOption( @@ -1525,36 +1649,36 @@ class RustdeskImpl { required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSetSharedOption"); } Future pluginReload({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginReload"); } void pluginEnable({required String id, required bool v, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginEnable"); } bool pluginIsEnabled({required String id, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginIsEnabled"); } bool pluginFeatureIsEnabled({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginFeatureIsEnabled"); } Future pluginSyncUi({required String syncTo, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginSyncUi"); } Future pluginListReload({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginListReload"); } Future pluginInstall( {required String id, required bool b, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("pluginInstall"); } bool isSupportMultiUiSession({required String version, dynamic hint}) { @@ -1566,88 +1690,142 @@ class RustdeskImpl { } String mainDefaultPrivacyModeImpl({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainDefaultPrivacyModeImpl"); } String mainSupportedPrivacyModeImpls({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSupportedPrivacyModeImpls"); } String mainSupportedInputSource({dynamic hint}) { return jsonEncode([ + ['Input source 1', 'input_source_1_tip'], ['Input source 2', 'input_source_2_tip'] ]); } Future mainGenerate2Fa({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGenerate2Fa"); } Future mainVerify2Fa({required String code, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVerify2Fa"); } bool mainHasValid2FaSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasValid2FaSync"); } String mainGetHardOption({required String key, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetHardOption"); } Future mainCheckHwcodec({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainCheckHwcodec"); } Future sessionRequestNewDisplayInitMsgs( {required UuidValue sessionId, required int display, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("sessionRequestNewDisplayInitMsgs"); } Future mainHandleWaylandScreencastRestoreToken( {required String key, required String value, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHandleWaylandScreencastRestoreToken"); } bool mainIsOptionFixed({required String key, dynamic hint}) { - throw UnimplementedError(); + return false; } bool mainGetUseTextureRender({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetUseTextureRender"); } bool mainHasValidBotSync({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainHasValidBotSync"); } Future mainVerifyBot({required String token, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainVerifyBot"); } String mainGetUnlockPin({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetUnlockPin"); } String mainSetUnlockPin({required String pin, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainSetUnlockPin"); } bool sessionGetEnableTrustedDevices( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return js.context.callMethod('getByName', ['enable_trusted_devices']) == + 'Y'; } Future mainGetTrustedDevices({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainGetTrustedDevices"); } Future mainRemoveTrustedDevices({required String json, dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainRemoveTrustedDevices"); } Future mainClearTrustedDevices({dynamic hint}) { - throw UnimplementedError(); + throw UnimplementedError("mainClearTrustedDevices"); + } + + Future getVoiceCallInputDevice({required bool isCm, dynamic hint}) { + throw UnimplementedError("getVoiceCallInputDevice"); + } + + Future setVoiceCallInputDevice( + {required bool isCm, required String device, dynamic hint}) { + throw UnimplementedError("setVoiceCallInputDevice"); + } + + bool isPresetPasswordMobileOnly({dynamic hint}) { + throw UnimplementedError("isPresetPasswordMobileOnly"); + } + + String mainGetBuildinOption({required String key, dynamic hint}) { + return ''; + } + + String installInstallOptions({dynamic hint}) { + throw UnimplementedError("installInstallOptions"); + } + + int mainMaxEncryptLen({dynamic hint}) { + throw UnimplementedError("mainMaxEncryptLen"); + } + + Future sessionRenameFile( + {required UuidValue sessionId, + required int actId, + required String path, + required String newName, + required bool isRemote, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'rename_file', + jsonEncode({ + 'id': actId, + 'path': path, + 'new_name': newName, + 'is_remote': isRemote + }) + ])); + } + + Future sessionSelectFiles( + {required UuidValue sessionId, dynamic hint}) { + return Future(() => js.context.callMethod('setByName', ['select_files'])); + } + + String? sessionGetConnToken({required UuidValue sessionId, dynamic hint}) { + throw UnimplementedError("sessionGetConnToken"); } void dispose() {} diff --git a/flutter/lib/web/common.dart b/flutter/lib/web/common.dart index 93b53f94802..4d539d5d47c 100644 --- a/flutter/lib/web/common.dart +++ b/flutter/lib/web/common.dart @@ -1,4 +1,7 @@ import 'dart:js' as js; +import 'dart:html' as html; +// cycle imports, maybe we can improve this +import 'package:flutter_hbb/consts.dart'; final isAndroid_ = false; final isIOS_ = false; @@ -11,3 +14,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile'); final isDesktop_ = false; String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']); + +final _localOs = js.context.callMethod('getByName', ['local_os', '']); +final isWebOnWindows_ = _localOs == kPeerPlatformWindows; +final isWebOnLinux_ = _localOs == kPeerPlatformLinux; +final isWebOnMacOS_ = _localOs == kPeerPlatformMacOS; diff --git a/flutter/lib/web/custom_cursor.dart b/flutter/lib/web/custom_cursor.dart index fd1fc4a18f8..54df77e98b5 100644 --- a/flutter/lib/web/custom_cursor.dart +++ b/flutter/lib/web/custom_cursor.dart @@ -58,6 +58,11 @@ class CursorManager { ]); } } + + Future resetSystemCursor() async { + latestKey = ''; + js.context.callMethod('setByName', ['cursor', 'auto']); + } } class FlutterCustomMemoryImageCursor extends MouseCursor { @@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession { } deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key); +resetSystemCursor() => CursorManager.instance.resetSystemCursor(); MouseCursor buildCursorOfCache( model.CursorModel cursor, double scale, model.CursorData? cache) { diff --git a/flutter/lib/web/dummy.dart b/flutter/lib/web/dummy.dart new file mode 100644 index 00000000000..b9e3b80b6ed --- /dev/null +++ b/flutter/lib/web/dummy.dart @@ -0,0 +1,14 @@ +Future webselectFiles({required bool is_folder}) async { + throw UnimplementedError("webselectFiles"); +} + +Future webSendLocalFiles( + {required int handleIndex, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote}) { + throw UnimplementedError("webSendLocalFiles"); +} diff --git a/flutter/lib/web/settings_page.dart b/flutter/lib/web/settings_page.dart new file mode 100644 index 00000000000..1cf23ecf9f8 --- /dev/null +++ b/flutter/lib/web/settings_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; + +class WebSettingsPage extends StatelessWidget { + const WebSettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return _buildDesktopButton(context); + } + + Widget _buildDesktopButton(BuildContext context) { + return IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + DesktopSettingPage(initialTabkey: SettingsTabKey.general), + ), + ); + }, + ); + } +} diff --git a/flutter/lib/web/texture_rgba_renderer.dart b/flutter/lib/web/texture_rgba_renderer.dart index 83407773583..9a4a1879b12 100644 --- a/flutter/lib/web/texture_rgba_renderer.dart +++ b/flutter/lib/web/texture_rgba_renderer.dart @@ -6,7 +6,7 @@ class TextureRgbaRenderer { } Future closeTexture(int key) { - throw UnimplementedError(); + return Future(() => true); } Future onRgba( diff --git a/flutter/lib/web/web_unique.dart b/flutter/lib/web/web_unique.dart new file mode 100644 index 00000000000..14774e668b0 --- /dev/null +++ b/flutter/lib/web/web_unique.dart @@ -0,0 +1,30 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:js' as js; + +Future webselectFiles({required bool is_folder}) async { + return Future( + () => js.context.callMethod('setByName', ['select_files', is_folder])); +} + +Future webSendLocalFiles( + {required int handleIndex, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote}) { + return Future(() => js.context.callMethod('setByName', [ + 'send_local_files', + jsonEncode({ + 'id': actId, + 'handle_index': handleIndex, + 'path': path, + 'to': to, + 'file_num': fileNum, + 'include_hidden': includeHidden, + 'is_remote': isRemote, + }) + ])); +} diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a9f3c7388cf..a29674fece3 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -95,17 +95,17 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index bbd91b045ce..6551bbb37bf 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "80b063b9d4e015f62e17f42a5aa0b3d20a365926" + resolved-ref: "519350f1f40746798299e94786197d058353bac9" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -380,6 +380,22 @@ packages: url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" + extended_text: + dependency: "direct main" + description: + name: extended_text + sha256: "7f382de3af12992e34bd72ddd36becf90c4720900af126cb9859f0189af71ffe" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + extended_text_library: + dependency: transitive + description: + name: extended_text_library + sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + url: "https://pub.dev" + source: hosted + version: "12.0.0" external_path: dependency: "direct main" description: @@ -509,8 +525,8 @@ packages: dependency: "direct main" description: path: "." - ref: "38951317afe79d953ab25733667bd96e172a80d3" - resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" + ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" + resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" @@ -1613,5 +1629,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 965e7f7bd16..cc3a2e6c53f 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.0+46 +version: 1.3.2+51 environment: sdk: '^3.1.0' @@ -93,7 +93,7 @@ dependencies: flutter_gpu_texture_renderer: git: url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer - ref: 38951317afe79d953ab25733667bd96e172a80d3 + ref: 2ded7f146437a761ffe6981e2f742038f85ca68d uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 @@ -104,6 +104,7 @@ dependencies: pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 + extended_text: 13.0.0 dev_dependencies: icons_launcher: ^2.0.4 diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 1a9a047578f..30055740ed8 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -5,7 +5,7 @@ use std::{ }; #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] -use hbb_common::{allow_err, log}; +use hbb_common::{allow_err, bail}; use hbb_common::{ lazy_static, tokio::sync::{ @@ -25,6 +25,8 @@ pub use context_send::*; const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; #[cfg(target_os = "windows")] const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; +#[cfg(target_os = "windows")] +const ERR_CODE_SEND_MSG: u32 = 0x00000003; pub(crate) use platform::create_cliprdr_context; @@ -130,7 +132,7 @@ impl ClipboardFile { ) } - pub fn is_stopping_allowed_from_peer(&self) -> bool { + pub fn is_beginning_message(&self) -> bool { matches!( self, ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } @@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType<()> { #[cfg(target_os = "windows")] return send_data_to_channel(conn_id, data); #[cfg(not(target_os = "windows"))] @@ -210,25 +212,28 @@ fn send_data(conn_id: i32, data: ClipboardFile) { } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[inline] -fn send_data_to_channel(conn_id: i32, data: ClipboardFile) { - // no need to handle result here +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { if let Some(msg_channel) = VEC_MSG_CHANNEL .read() .unwrap() .iter() .find(|x| x.conn_id == conn_id) { - allow_err!(msg_channel.sender.send(data)); + msg_channel.sender.send(data)?; + Ok(()) + } else { + bail!("conn_id not found"); } } #[cfg(feature = "unix-file-copy-paste")] #[inline] -fn send_data_to_all(data: ClipboardFile) { - // no need to handle result here +fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { + // Need more tests to see if it's necessary to handle the error. for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { allow_err!(msg_channel.sender.send(data.clone())); } + Ok(()) } #[cfg(test)] diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 8fc917c6fee..5d1aa086ddb 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -7,7 +7,7 @@ use crate::{ allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, - ERR_CODE_INVALID_PARAMETER, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, + ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, }; use hbb_common::log; use std::{ @@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) } }; // no need to handle result here - send_data(conn_id as _, data); + allow_err!(send_data(conn_id as _, data)); 0 } @@ -1045,7 +1045,13 @@ extern "C" fn client_format_list( .iter() .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); } else { - send_data(conn_id, data); + match send_data(conn_id, data) { + Ok(_) => {} + Err(e) => { + log::error!("failed to send format list: {:?}", e); + return ERR_CODE_SEND_MSG; + } + } } 0 @@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response( msg_flags ); let data = ClipboardFile::FormatListResponse { msg_flags }; - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format list response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_format_data_request( @@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request( conn_id, requested_format_id ); - // no need to handle result here - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data request: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_format_data_response( @@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response( msg_flags, format_data, }; - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_file_contents_request( @@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request( clip_data_id, }; log::debug!("client_file_contents_request called, data: {:?}", &data); - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents request: {:?}", e); + ERR_CODE_SEND_MSG + } + } } extern "C" fn client_file_contents_response( @@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response( msg_flags, stream_id ); - send_data(conn_id, data); - - 0 + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents response: {:?}", e); + ERR_CODE_SEND_MSG + } + } } diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index c8f2038a1d2..c2b7556a46a 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -220,7 +220,8 @@ struct wf_clipboard HWND hwnd; HANDLE hmem; HANDLE thread; - HANDLE response_data_event; + HANDLE formatDataRespEvent; + BOOL formatDataRespReceived; LPDATAOBJECT data_obj; HANDLE data_obj_mutex; @@ -228,6 +229,7 @@ struct wf_clipboard ULONG req_fsize; char *req_fdata; HANDLE req_fevent; + BOOL req_f_received; size_t nFiles; size_t file_array_size; @@ -287,6 +289,9 @@ static BOOL try_open_clipboard(HWND hwnd) static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid, void **ppvObject) { + if (ppvObject == NULL) + return E_INVALIDARG; + if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) { IStream_AddRef(This); @@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO } *pcbRead = clipboard->req_fsize; + // Check overflow, can not be a real case + if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) { + // It's better to crash to release the explorer.exe + // This is a critical error, because the explorer is waiting for the data + // and the m_lOffset is wrong(overflowed) + return S_FALSE; + } instance->m_lOffset.QuadPart += clipboard->req_fsize; if (clipboard->req_fsize < cb) @@ -517,11 +529,17 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **pp static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc) { - IStream *iStream; + IStream *iStream = NULL; BOOL success = FALSE; BOOL isDir = FALSE; - CliprdrStream *instance; + CliprdrStream *instance = NULL; wfClipboard *clipboard = (wfClipboard *)pData; + + if (!(pData && dsc)) + { + return NULL; + } + instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); if (instance) @@ -874,14 +892,18 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count, void *data) { - CliprdrDataObject *instance; - IDataObject *iDataObject; + CliprdrDataObject *instance = NULL; + IDataObject *iDataObject = NULL; instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); if (!instance) goto error; + instance->m_pFormatEtc = NULL; + instance->m_pStgMedium = NULL; + iDataObject = &instance->iDataObject; + iDataObject->lpVtbl = NULL; iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); if (!iDataObject->lpVtbl) @@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc return instance; error: - CliprdrDataObject_Delete(instance); + if (iDataObject && iDataObject->lpVtbl) + { + free(iDataObject->lpVtbl); + } + if (instance) + { + if (instance->m_pFormatEtc) + { + free(instance->m_pFormatEtc); + } + + if (instance->m_pStgMedium) + { + free(instance->m_pStgMedium); + } + + CliprdrDataObject_Delete(instance); + } return NULL; } @@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT REFIID riid, void **ppvObject) { (void)This; + if (!ppvObject) + return E_INVALIDARG; if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) { @@ -1198,6 +1239,7 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f WCHAR *unicode_name; #if !defined(UNICODE) size_t size; + int towchar_count; #endif if (!clipboard || !format_name) @@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f #if defined(UNICODE) unicode_name = _wcsdup(format_name); + if (!unicode_name) + return 0; #else size = _tcslen(format_name); unicode_name = calloc(size + 1, sizeof(WCHAR)); @@ -1212,11 +1256,13 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f if (!unicode_name) return 0; - MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); -#endif - - if (!unicode_name) + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0); + if (towchar_count <= 0 || towchar_count > size) + return 0; + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); + if (towchar_count <= 0) return 0; +#endif for (i = 0; i < clipboard->map_size; i++) { @@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard) if (!clipboard) return -1; + // to-do: + // Directly use the environment variable `TEMP` is not safe. + // But this function is not used for now. if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == 0) return -1; @@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) return rc; } -UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, void **data) +// Ensure the event is not signaled, and reset it if it is. +UINT try_reset_event(HANDLE event) +{ + if (!event) + { + return ERROR_INTERNAL_ERROR; + } + + DWORD result = WaitForSingleObject(event, 0); + if (result == WAIT_OBJECT_0) + { + if (!ResetEvent(event)) + { + return GetLastError(); + } + else + { + return ERROR_SUCCESS; + } + } + else if (result == WAIT_TIMEOUT) + { + return ERROR_SUCCESS; + } + else + { + return ERROR_INTERNAL_ERROR; + } +} + +UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data) { UINT rc = ERROR_SUCCESS; clipboard->context->IsStopped = FALSE; @@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis); if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE) { - continue; + if ((*recvedFlag) == TRUE) { + // The data has been received, but the event is still not signaled. + // We just skip the rest of the waiting and reset the flag. + *recvedFlag = FALSE; + // Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data. + waitRes = WAIT_OBJECT_0; + } else { + // The data has not been received yet, we should continue to wait. + continue; + } + } + + if (!ResetEvent(event)) + { + // NOTE: critical error here, crash may be better } if (clipboard->context->IsStopped == TRUE) @@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo return ERROR_INTERNAL_ERROR; } - if (!ResetEvent(event)) - { - // NOTE: critical error here, crash may be better - rc = ERROR_INTERNAL_ERROR; - } - if ((*data) == NULL) { rc = ERROR_INTERNAL_ERROR; @@ -1519,6 +1606,13 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) return ERROR_INTERNAL_ERROR; + rc = try_reset_event(clipboard->formatDataRespEvent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->formatDataRespReceived = FALSE; + remoteFormatId = get_remote_format_id(clipboard, formatId); formatDataRequest.connID = connID; @@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN return rc; } - wait_response_event(connID, clipboard, clipboard->response_data_event, &clipboard->hmem); + return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem); } UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index, @@ -1543,7 +1637,17 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) return ERROR_INTERNAL_ERROR; + rc = try_reset_event(clipboard->req_fevent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->req_f_received = FALSE; + fileContentsRequest.connID = connID; + // streamId is `IStream*` pointer, though it is not very good on a 64-bit system. + // But it is OK, because it is only used to check if the stream is the same in + // `wf_cliprdr_server_file_contents_request()` function. fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid; fileContentsRequest.listIndex = index; fileContentsRequest.dwFlags = flag; @@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co return rc; } - return wait_response_event(connID, clipboard, clipboard->req_fevent, (void **)&clipboard->req_fdata); + return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata); } static UINT cliprdr_send_response_filecontents( @@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM break; case WM_DESTROYCLIPBOARD: + // to-do: clear clipboard data? case WM_ASKCBFORMATNAME: case WM_HSCROLLCLIPBOARD: case WM_PAINTCLIPBOARD: @@ -1904,7 +2009,7 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po LONG positionHigh, DWORD nRequested, DWORD *puSize) { BOOL res = FALSE; - HANDLE hFile; + HANDLE hFile = NULL; DWORD nGet, rc; if (!file_name || !buffer || !puSize) @@ -1932,9 +2037,11 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po res = TRUE; error: - - if (!CloseHandle(hFile)) - res = FALSE; + if (hFile) + { + if (!CloseHandle(hFile)) + res = FALSE; + } if (res) *puSize = nGet; @@ -1945,8 +2052,8 @@ static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG po /* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen) { - HANDLE hFile; - FILEDESCRIPTORW *fd; + HANDLE hFile = NULL; + FILEDESCRIPTORW *fd = NULL; fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); if (!fd) @@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t } fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); - wcscpy_s(fd->cFileName, sizeof(fd->cFileName) / 2, file_name + pathLen); + if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0])) + { + // The file name is too long, which is not a normal case. + // So we just return NULL. + CloseHandle(hFile); + free(fd); + return NULL; + } + + wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1); CloseHandle(hFile); return fd; @@ -2024,7 +2140,12 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi if (!clipboard->file_names[clipboard->nFiles]) return FALSE; - wcscpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name); + // `MAX_PATH` is long enough for the file name. + // So we just return FALSE if the file name is too long, which is not a normal case. + if ((wcslen(full_file_name) + 1) > MAX_PATH) + return FALSE; + + wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1); /* add to descriptor array */ clipboard->fileDescriptor[clipboard->nFiles] = wf_cliprdr_get_file_descriptor(full_file_name, pathLen); @@ -2048,8 +2169,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si if (!clipboard || !Dir) return FALSE; - // StringCchCopy(DirSpec, MAX_PATH, Dir); - // StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); + if (wcslen(Dir) + 3 > MAX_PATH) + return FALSE; StringCchCopyW(DirSpec, MAX_PATH, Dir); StringCchCatW(DirSpec, MAX_PATH, L"\\*"); @@ -2078,9 +2199,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { WCHAR DirAdd[MAX_PATH]; - // StringCchCopy(DirAdd, MAX_PATH, Dir); - // StringCchCat(DirAdd, MAX_PATH, _T("\\")); - // StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName); + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; StringCchCopyW(DirAdd, MAX_PATH, Dir); StringCchCatW(DirAdd, MAX_PATH, L"\\"); StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); @@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si else { WCHAR fileName[MAX_PATH]; - // StringCchCopy(fileName, MAX_PATH, Dir); - // StringCchCat(fileName, MAX_PATH, _T("\\")); - // StringCchCat(fileName, MAX_PATH, FindFileData.cFileName); - + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; StringCchCopyW(fileName, MAX_PATH, Dir); StringCchCatW(fileName, MAX_PATH, L"\\"); StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); @@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, if (context->EnableFiles) { UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32)); - *p_conn_id = formatList->connID; - if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) - rc = CHANNEL_RC_OK; + if (p_conn_id) { + *p_conn_id = formatList->connID; + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) + rc = CHANNEL_RC_OK; + } } else { @@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); - format_ids->connID = formatList->connID; - format_ids->size = (UINT32)clipboard->map_size; - format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); - for (i = 0; i < format_ids->size; ++i) + if (format_ids) { - format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; - } - if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) - { - rc = CHANNEL_RC_OK; + format_ids->connID = formatList->connID; + format_ids->size = (UINT32)clipboard->map_size; + format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); + if (format_ids->formats) + { + for (i = 0; i < format_ids->size; ++i) + { + format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; + } + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) + { + rc = CHANNEL_RC_OK; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + else + { + rc = ERROR_INTERNAL_ERROR; + } } else { @@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, p += len + 1, clipboard->nFiles++) { int cchWideChar; - WCHAR *wFileName; cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); - MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); - wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + if (wFileName) + { + MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); + wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + free(wFileName); + } + else + { + rc = ERROR_INTERNAL_ERROR; + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + goto exit; + } } } GlobalUnlock(stg_medium.hGlobal); ReleaseStgMedium(&stg_medium); resp: + // size will not overflow, because size type is size_t (unsigned __int64) size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); @@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, globlemem = (char *)GlobalLock(hClipdata); size = (int)GlobalSize(hClipdata); buff = malloc(size); - CopyMemory(buff, globlemem, size); + if (buff) + { + CopyMemory(buff, globlemem, size); + rc = ERROR_SUCCESS; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } GlobalUnlock(hClipdata); CloseClipboard(); - rc = ERROR_SUCCESS; } } else @@ -2545,7 +2697,7 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context, response.requestedFormatData = (BYTE *)buff; if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response)) { - // CAUTION: if failed to send, server will wait a long time + // CAUTION: if failed to send, server will wait a long time, default 30 seconds. } if (buff) @@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context, rc = CHANNEL_RC_OK; } while (0); - if (!SetEvent(clipboard->response_data_event)) + if (!SetEvent(clipboard->formatDataRespEvent)) { - // CAUTION: critical error here, process will hang up until wait timeout default 3min. + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->formatDataRespReceived = TRUE; rc = ERROR_INTERNAL_ERROR; } return rc; @@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, if (!SetEvent(clipboard->req_fevent)) { - // CAUTION: critical error here, process will hang up until wait timeout default 3min. + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->req_f_received = TRUE; } return rc; } @@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) goto error; - if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) + if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) goto error; + clipboard->formatDataRespReceived = FALSE; if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex"))) goto error; if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) goto error; + clipboard->req_f_received = FALSE; if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) goto error; @@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) clipboard->data_obj = NULL; } - if (clipboard->response_data_event) - CloseHandle(clipboard->response_data_event); + if (clipboard->formatDataRespEvent) + CloseHandle(clipboard->formatDataRespEvent); if (clipboard->data_obj_mutex) CloseHandle(clipboard->data_obj_mutex); diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index b56beff129f..e7d7d9e8d33 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3; const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; const BUF_LEN: usize = 4; +const MOUSE_EVENT_BUTTON_NUMBER_BACK: i64 = 3; +const MOUSE_EVENT_BUTTON_NUMBER_FORWARD: i64 = 4; + /// The event source user data value of cgevent. pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100; @@ -108,11 +111,17 @@ pub struct Enigo { double_click_interval: u32, last_click_time: Option, multiple_click: i64, + ignore_flags: bool, flags: CGEventFlags, char_to_vkey_map: Map>, } impl Enigo { + /// Set if ignore flags when posting events. + pub fn set_ignore_flags(&mut self, ignore: bool) { + self.ignore_flags = ignore; + } + /// pub fn reset_flag(&mut self) { self.flags = CGEventFlags::CGEventFlagNull; @@ -133,7 +142,9 @@ impl Enigo { } fn post(&self, event: CGEvent) { - event.set_flags(self.flags); + if !self.ignore_flags { + event.set_flags(self.flags); + } event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE); event.post(CGEventTapLocation::HID); } @@ -161,6 +172,7 @@ impl Default for Enigo { double_click_interval, multiple_click: 1, last_click_time: None, + ignore_flags: false, flags: CGEventFlags::CGEventFlagNull, char_to_vkey_map: Default::default(), } @@ -226,14 +238,24 @@ impl MouseControllable for Enigo { } self.last_click_time = Some(now); let (current_x, current_y) = Self::mouse_location(); - let (button, event_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), _ => { log::info!("Unsupported button {:?}", button); return Ok(()); - }, + } }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -244,6 +266,9 @@ impl MouseControllable for Enigo { self.multiple_click, ); } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } self.post(event); } } @@ -252,14 +277,24 @@ impl MouseControllable for Enigo { fn mouse_up(&mut self, button: MouseButton) { let (current_x, current_y) = Self::mouse_location(); - let (button, event_type) = match button { - MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), - MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), - MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), _ => { log::info!("Unsupported button {:?}", button); return; - }, + } }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -270,6 +305,9 @@ impl MouseControllable for Enigo { self.multiple_click, ); } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } self.post(event); } } @@ -345,7 +383,7 @@ impl KeyboardControllable for Enigo { fn as_mut_any(&mut self) -> &mut dyn std::any::Any { self } - + fn key_sequence(&mut self, sequence: &str) { // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time @@ -382,12 +420,10 @@ impl KeyboardControllable for Enigo { fn key_down(&mut self, key: Key) -> crate::ResultType { let code = self.key_to_keycode(key); if code == u16::MAX { - return Err("".into()); + return Err("".into()); } if let Some(src) = self.event_source.as_ref() { - if let Ok(event) = - CGEvent::new_keyboard_event(src.clone(), code, true) - { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) { self.post(event); } } diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 4554617a7d6..21f9e7aea0d 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -326,6 +326,7 @@ enum ClipboardFormat { ImageRgba = 21; ImagePng = 22; ImageSvg = 23; + Special = 31; } message Clipboard { @@ -334,6 +335,8 @@ message Clipboard { int32 width = 3; int32 height = 4; ClipboardFormat format = 5; + // Special format name, only used when format is Special. + string special_name = 6; } message MultiClipboards { repeated Clipboard clipboards = 1; } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 86d24e13833..81707622f43 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -39,7 +39,7 @@ pub const REG_INTERVAL: i64 = 15_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; const PASSWORD_ENC_VERSION: &str = "00"; -const ENCRYPT_MAX_LEN: usize = 128; +pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all #[cfg(target_os = "macos")] lazy_static::lazy_static! { @@ -296,6 +296,8 @@ pub struct PeerConfig { pub keyboard_mode: String, #[serde(flatten)] pub view_only: ViewOnly, + #[serde(flatten)] + pub sync_init_clipboard: SyncInitClipboard, // Mouse wheel or touchpad scroll mode #[serde( default = "PeerConfig::default_reverse_mouse_wheel", @@ -373,6 +375,7 @@ impl Default for PeerConfig { ui_flutter: Default::default(), info: Default::default(), transfer: Default::default(), + sync_init_clipboard: Default::default(), } } } @@ -962,6 +965,10 @@ impl Config { .unwrap_or_default() } + pub fn get_bool_option(k: &str) -> bool { + option2bool(k, &Self::get_option(k)) + } + pub fn set_option(k: String, v: String) { if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) { return; @@ -1462,6 +1469,13 @@ serde_field_bool!( "ViewOnly::default_view_only" ); +serde_field_bool!( + SyncInitClipboard, + "sync-init-clipboard", + default_sync_init_clipboard, + "SyncInitClipboard::default_sync_init_clipboard" +); + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { #[serde(default, deserialize_with = "deserialize_string")] @@ -1548,6 +1562,21 @@ impl LocalConfig { .unwrap_or_default() } + // Usually get_option should be used. + pub fn get_option_from_file(k: &str) -> String { + get_or( + &OVERWRITE_LOCAL_SETTINGS, + &Self::load().options, + &DEFAULT_LOCAL_SETTINGS, + k, + ) + .unwrap_or_default() + } + + pub fn get_bool_option(k: &str) -> bool { + option2bool(k, &Self::get_option(k)) + } + pub fn set_option(k: String, v: String) { if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) { return; @@ -2156,6 +2185,7 @@ pub mod keys { pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; + pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; pub const OPTION_THEME: &str = "theme"; pub const OPTION_LANGUAGE: &str = "lang"; pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; @@ -2187,6 +2217,7 @@ pub mod keys { pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout"; pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open"; pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming"; + pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing"; pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory"; pub const OPTION_ENABLE_ABR: &str = "enable-abr"; pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper"; @@ -2218,6 +2249,9 @@ pub mod keys { pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; pub const OPTION_HIDE_TRAY: &str = "hide-tray"; + pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; + pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; + pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; // flutter local options pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; @@ -2276,6 +2310,7 @@ pub mod keys { OPTION_CUSTOM_IMAGE_QUALITY, OPTION_CUSTOM_FPS, OPTION_CODEC_PREFERENCE, + OPTION_SYNC_INIT_CLIPBOARD, ]; // DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ @@ -2306,6 +2341,8 @@ pub mod keys { OPTION_DISABLE_GROUP_PANEL, OPTION_PRE_ELEVATE_SERVICE, OPTION_ALLOW_REMOTE_CM_MODIFICATION, + OPTION_ALLOW_AUTO_RECORD_OUTGOING, + OPTION_VIDEO_SAVE_DIRECTORY, ]; // DEFAULT_SETTINGS, OVERWRITE_SETTINGS pub const KEYS_SETTINGS: &[&str] = &[ @@ -2327,7 +2364,6 @@ pub mod keys { OPTION_AUTO_DISCONNECT_TIMEOUT, OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, OPTION_ALLOW_AUTO_RECORD_INCOMING, - OPTION_VIDEO_SAVE_DIRECTORY, OPTION_ENABLE_ABR, OPTION_ALLOW_REMOVE_WALLPAPER, OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, @@ -2362,6 +2398,9 @@ pub mod keys { OPTION_HIDE_HELP_CARDS, OPTION_DEFAULT_CONNECT_PASSWORD, OPTION_HIDE_TRAY, + OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, + OPTION_ALLOW_LOGON_SCREEN_PASSWORD, + OPTION_ONE_WAY_FILE_TRANSFER, ]; } diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 49a2d4d9498..5c04cc97b92 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -89,11 +89,11 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String log::error!("Duplicate encryption!"); return s.to_owned(); } - if s.bytes().len() > max_len { + if s.chars().count() > max_len { return String::default(); } if version == "00" { - if let Ok(s) = encrypt(s.as_bytes(), max_len) { + if let Ok(s) = encrypt(s.as_bytes()) { return version.to_owned() + &s; } } @@ -130,7 +130,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec (Vec, boo (v.to_owned(), false, !v.is_empty()) } -fn encrypt(v: &[u8], max_len: usize) -> Result { - if !v.is_empty() && v.len() <= max_len { +fn encrypt(v: &[u8]) -> Result { + if !v.is_empty() { symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) } else { Err(()) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 5e03b6816e4..60c8714d821 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -7,6 +7,9 @@ lazy_static::lazy_static! { pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; pub const DISPLAY_SERVER_X11: &str = "x11"; +pub const DISPLAY_DESKTOP_KDE: &str = "KDE"; + +pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; pub struct Distro { pub name: String, @@ -29,6 +32,15 @@ impl Distro { } } +#[inline] +pub fn is_kde() -> bool { + if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) { + env == DISPLAY_DESKTOP_KDE + } else { + false + } +} + #[inline] pub fn is_gdm_user(username: &str) -> bool { username == "gdm" diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index ce1c10c09e4..7e60f7d1fac 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.0" +version = "1.3.2" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 3a0784e7cca..529010f1607 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -62,4 +62,3 @@ gstreamer-video = { version = "0.16", optional = true } git = "https://github.com/rustdesk-org/hwcodec" optional = true - diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 07ff0f91d24..dad924c862c 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -15,7 +15,7 @@ use crate::{ aom::{self, AomDecoder, AomEncoder, AomEncoderConfig}, common::GoogleImage, vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId}, - CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, + CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture, }; use hbb_common::{ @@ -623,7 +623,7 @@ impl Decoder { &mut self, frame: &video_frame::Union, rgb: &mut ImageRgb, - _texture: &mut *mut c_void, + _texture: &mut ImageTexture, _pixelbuffer: &mut bool, chroma: &mut Option, ) -> ResultType { @@ -777,12 +777,16 @@ impl Decoder { fn handle_vram_video_frame( decoder: &mut VRamDecoder, frames: &EncodedVideoFrames, - texture: &mut *mut c_void, + texture: &mut ImageTexture, ) -> ResultType { let mut ret = false; for h26x in frames.frames.iter() { for image in decoder.decode(&h26x.data)? { - *texture = image.frame.texture; + *texture = ImageTexture { + texture: image.frame.texture, + w: image.frame.width as _, + h: image.frame.height as _, + }; ret = true; } } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 4e653215eb6..a0e730c91db 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -498,6 +498,15 @@ pub struct HwCodecConfig { pub vram_decode: Vec, } +// HwCodecConfig2 is used to store the config in json format, +// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version. +// struct T { a: Vec, b: Vec} will fail if b is empty, but struct T { a: Vec, b: Vec} is ok. +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +struct HwCodecConfig2 { + #[serde(default)] + pub config: String, +} + // ipc server process start check process once, other process get from ipc server once // install: --server start check process, check process send to --server, ui get from --server // portable: ui start check process, check process send to ui @@ -509,7 +518,12 @@ impl HwCodecConfig { log::info!("set hwcodec config"); log::debug!("{config:?}"); #[cfg(any(windows, target_os = "macos"))] - hbb_common::config::common_store(&config, "_hwcodec"); + hbb_common::config::common_store( + &HwCodecConfig2 { + config: serde_json::to_string_pretty(&config).unwrap_or_default(), + }, + "_hwcodec", + ); *CONFIG.lock().unwrap() = Some(config); *CONFIG_SET_BY_IPC.lock().unwrap() = true; } @@ -587,7 +601,8 @@ impl HwCodecConfig { Some(c) => c, None => { log::info!("try load cached hwcodec config"); - let c = hbb_common::config::common_load::("_hwcodec"); + let c = hbb_common::config::common_load::("_hwcodec"); + let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default(); let new_signature = hwcodec::common::get_gpu_signature(); if c.signature == new_signature { log::debug!("load cached hwcodec config: {c:?}"); diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 164f7157de3..ee96f57c851 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -96,6 +96,22 @@ impl ImageRgb { } } +pub struct ImageTexture { + pub texture: *mut c_void, + pub w: usize, + pub h: usize, +} + +impl Default for ImageTexture { + fn default() -> Self { + Self { + texture: std::ptr::null_mut(), + w: 0, + h: 0, + } + } +} + #[inline] pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { // does this really help? @@ -156,7 +172,7 @@ pub trait TraitPixelBuffer { #[cfg(not(any(target_os = "ios")))] pub enum Frame<'a> { PixelBuffer(PixelBuffer<'a>), - Texture(*mut c_void), + Texture((*mut c_void, usize)), } #[cfg(not(any(target_os = "ios")))] @@ -164,7 +180,7 @@ impl Frame<'_> { pub fn valid<'a>(&'a self) -> bool { match self { Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(), - Frame::Texture(texture) => !texture.is_null(), + Frame::Texture((texture, _)) => !texture.is_null(), } } @@ -186,7 +202,7 @@ impl Frame<'_> { pub enum EncodeInput<'a> { YUV(&'a [u8]), - Texture(*mut c_void), + Texture((*mut c_void, usize)), } impl<'a> EncodeInput<'a> { @@ -197,7 +213,7 @@ impl<'a> EncodeInput<'a> { } } - pub fn texture(&self) -> ResultType<*mut c_void> { + pub fn texture(&self) -> ResultType<(*mut c_void, usize)> { match self { Self::Texture(f) => Ok(*f), _ => bail!("not texture frame"), @@ -296,6 +312,19 @@ impl From<&VideoFrame> for CodecFormat { } } +impl From<&video_frame::Union> for CodecFormat { + fn from(it: &video_frame::Union) -> Self { + match it { + video_frame::Union::Vp8s(_) => CodecFormat::VP8, + video_frame::Union::Vp9s(_) => CodecFormat::VP9, + video_frame::Union::Av1s(_) => CodecFormat::AV1, + video_frame::Union::H264s(_) => CodecFormat::H264, + video_frame::Union::H265s(_) => CodecFormat::H265, + _ => CodecFormat::Unknown, + } + } +} + impl From<&CodecName> for CodecFormat { fn from(value: &CodecName) -> Self { match value { @@ -316,7 +345,7 @@ impl ToString for CodecFormat { CodecFormat::AV1 => "AV1".into(), CodecFormat::H264 => "H264".into(), CodecFormat::H265 => "H265".into(), - CodecFormat::Unknown => "Unknow".into(), + CodecFormat::Unknown => "Unknown".into(), } } } diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 52973c2b244..c53b7743147 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -25,22 +25,28 @@ pub struct RecorderContext { pub server: bool, pub id: String, pub dir: String, + pub display: usize, + pub tx: Option>, +} + +#[derive(Debug, Clone)] +pub struct RecorderContext2 { pub filename: String, pub width: usize, pub height: usize, pub format: CodecFormat, - pub tx: Option>, } -impl RecorderContext { - pub fn set_filename(&mut self) -> ResultType<()> { - if !PathBuf::from(&self.dir).exists() { - std::fs::create_dir_all(&self.dir)?; +impl RecorderContext2 { + pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> { + if !PathBuf::from(&ctx.dir).exists() { + std::fs::create_dir_all(&ctx.dir)?; } - let file = if self.server { "incoming" } else { "outgoing" }.to_string() + let file = if ctx.server { "incoming" } else { "outgoing" }.to_string() + "_" - + &self.id.clone() + + &ctx.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() + + &format!("display{}_", ctx.display) + &self.format.to_string().to_lowercase() + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 @@ -50,11 +56,10 @@ impl RecorderContext { } else { ".mp4" }; - self.filename = PathBuf::from(&self.dir) + self.filename = PathBuf::from(&ctx.dir) .join(file) .to_string_lossy() .to_string(); - log::info!("video will save to {}", self.filename); Ok(()) } } @@ -63,7 +68,7 @@ unsafe impl Send for Recorder {} unsafe impl Sync for Recorder {} pub trait RecorderApi { - fn new(ctx: RecorderContext) -> ResultType + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType where Self: Sized; fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; @@ -78,13 +83,15 @@ pub enum RecordState { } pub struct Recorder { - pub inner: Box, + pub inner: Option>, ctx: RecorderContext, + ctx2: Option, pts: Option, + check_failed: bool, } impl Deref for Recorder { - type Target = Box; + type Target = Option>; fn deref(&self) -> &Self::Target { &self.inner @@ -98,114 +105,123 @@ impl DerefMut for Recorder { } impl Recorder { - pub fn new(mut ctx: RecorderContext) -> ResultType { - ctx.set_filename()?; - let recorder = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder { - inner: Box::new(WebmRecorder::new(ctx.clone())?), - ctx, - pts: None, - }, - #[cfg(feature = "hwcodec")] - _ => Recorder { - inner: Box::new(HwRecorder::new(ctx.clone())?), - ctx, - pts: None, - }, - #[cfg(not(feature = "hwcodec"))] - _ => bail!("unsupported codec type"), - }; - recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone())); - Ok(recorder) + pub fn new(ctx: RecorderContext) -> ResultType { + Ok(Self { + inner: None, + ctx, + ctx2: None, + pts: None, + check_failed: false, + }) } - fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { - ctx.set_filename()?; - self.inner = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => { - Box::new(WebmRecorder::new(ctx.clone())?) + fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { + match self.ctx2 { + Some(ref ctx2) => { + if ctx2.width != w || ctx2.height != h || ctx2.format != format { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } } - #[cfg(feature = "hwcodec")] - _ => Box::new(HwRecorder::new(ctx.clone())?), - #[cfg(not(feature = "hwcodec"))] - _ => bail!("unsupported codec type"), + None => { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } + } + let Some(ctx2) = &self.ctx2 else { + bail!("ctx2 is None"); }; - self.ctx = ctx; - self.pts = None; - self.send_state(RecordState::NewFile(self.ctx.filename.clone())); + if self.inner.is_none() { + self.inner = match format { + CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new( + WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?, + )), + #[cfg(feature = "hwcodec")] + _ => Some(Box::new(HwRecorder::new( + self.ctx.clone(), + (*ctx2).clone(), + )?)), + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + self.pts = None; + self.send_state(RecordState::NewFile(ctx2.filename.clone())); + } Ok(()) } - pub fn write_message(&mut self, msg: &Message) { + pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) { if let Some(message::Union::VideoFrame(vf)) = &msg.union { if let Some(frame) = &vf.union { - self.write_frame(frame).ok(); + self.write_frame(frame, w, h).ok(); } } } - pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { + pub fn write_frame( + &mut self, + frame: &video_frame::Union, + w: usize, + h: usize, + ) -> ResultType<()> { + if self.check_failed { + bail!("check failed"); + } + let format = CodecFormat::from(frame); + if format == CodecFormat::Unknown { + bail!("unsupported frame type"); + } + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } match frame { video_frame::Union::Vp8s(vp8s) => { - if self.ctx.format != CodecFormat::VP8 { - self.change(RecorderContext { - format: CodecFormat::VP8, - ..self.ctx.clone() - })?; - } for f in vp8s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Vp9s(vp9s) => { - if self.ctx.format != CodecFormat::VP9 { - self.change(RecorderContext { - format: CodecFormat::VP9, - ..self.ctx.clone() - })?; - } for f in vp9s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } video_frame::Union::Av1s(av1s) => { - if self.ctx.format != CodecFormat::AV1 { - self.change(RecorderContext { - format: CodecFormat::AV1, - ..self.ctx.clone() - })?; - } for f in av1s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { - if self.ctx.format != CodecFormat::H264 { - self.change(RecorderContext { - format: CodecFormat::H264, - ..self.ctx.clone() - })?; - } for f in h264s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { - if self.ctx.format != CodecFormat::H265 { - self.change(RecorderContext { - format: CodecFormat::H265, - ..self.ctx.clone() - })?; - } for f in h265s.frames.iter() { - self.check_pts(f.pts)?; - self.write_video(f); + self.check_pts(f.pts, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); } } _ => bail!("unsupported frame type"), @@ -214,13 +230,21 @@ impl Recorder { Ok(()) } - fn check_pts(&mut self, pts: i64) -> ResultType<()> { + fn check_pts(&mut self, pts: i64, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { // https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c let old_pts = self.pts; self.pts = Some(pts); if old_pts.clone().unwrap_or_default() > pts { log::info!("pts {:?} -> {}, change record filename", old_pts, pts); - self.change(self.ctx.clone())?; + self.inner = None; + self.ctx2 = None; + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } + self.pts = Some(pts); } Ok(()) } @@ -234,21 +258,22 @@ struct WebmRecorder { vt: VideoTrack, webm: Option>>, ctx: RecorderContext, + ctx2: RecorderContext2, key: bool, written: bool, start: Instant, } impl RecorderApi for WebmRecorder { - fn new(ctx: RecorderContext) -> ResultType { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { let out = match { OpenOptions::new() .write(true) .create_new(true) - .open(&ctx.filename) + .open(&ctx2.filename) } { Ok(file) => file, - Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?, Err(e) => return Err(e.into()), }; let mut webm = match mux::Segment::new(mux::Writer::new(out)) { @@ -256,18 +281,18 @@ impl RecorderApi for WebmRecorder { None => bail!("Failed to create webm mux"), }; let vt = webm.add_video_track( - ctx.width as _, - ctx.height as _, + ctx2.width as _, + ctx2.height as _, None, - if ctx.format == CodecFormat::VP9 { + if ctx2.format == CodecFormat::VP9 { mux::VideoCodecId::VP9 - } else if ctx.format == CodecFormat::VP8 { + } else if ctx2.format == CodecFormat::VP8 { mux::VideoCodecId::VP8 } else { mux::VideoCodecId::AV1 }, ); - if ctx.format == CodecFormat::AV1 { + if ctx2.format == CodecFormat::AV1 { // [129, 8, 12, 0] in 3.6.0, but zero works let codec_private = vec![0, 0, 0, 0]; if !webm.set_codec_private(vt.track_number(), &codec_private) { @@ -278,6 +303,7 @@ impl RecorderApi for WebmRecorder { vt, webm: Some(webm), ctx, + ctx2, key: false, written: false, start: Instant::now(), @@ -307,7 +333,7 @@ impl Drop for WebmRecorder { let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { - std::fs::remove_file(&self.ctx.filename).ok(); + std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } self.ctx.tx.as_ref().map(|tx| tx.send(state)); @@ -318,6 +344,7 @@ impl Drop for WebmRecorder { struct HwRecorder { muxer: Muxer, ctx: RecorderContext, + ctx2: RecorderContext2, written: bool, key: bool, start: Instant, @@ -325,18 +352,19 @@ struct HwRecorder { #[cfg(feature = "hwcodec")] impl RecorderApi for HwRecorder { - fn new(ctx: RecorderContext) -> ResultType { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { let muxer = Muxer::new(MuxContext { - filename: ctx.filename.clone(), - width: ctx.width, - height: ctx.height, - is265: ctx.format == CodecFormat::H265, + filename: ctx2.filename.clone(), + width: ctx2.width, + height: ctx2.height, + is265: ctx2.format == CodecFormat::H265, framerate: crate::hwcodec::DEFAULT_FPS as _, }) .map_err(|_| anyhow!("Failed to create hardware muxer"))?; Ok(HwRecorder { muxer, ctx, + ctx2, written: false, key: false, start: Instant::now(), @@ -365,7 +393,7 @@ impl Drop for HwRecorder { self.muxer.write_tail().ok(); let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { - std::fs::remove_file(&self.ctx.filename).ok(); + std::fs::remove_file(&self.ctx2.filename).ok(); state = RecordState::RemoveFile; } self.ctx.tx.as_ref().map(|tx| tx.send(state)); diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index a2b4d348c46..aae961df614 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -101,7 +101,12 @@ impl EncoderApi for VRamEncoder { frame: EncodeInput, ms: i64, ) -> ResultType { - let texture = frame.texture()?; + let (texture, rotation) = frame.texture()?; + if rotation != 0 { + // to-do: support rotation + // Both the encoder and display(w,h) information need to be changed. + bail!("rotation not supported"); + } let mut vf = VideoFrame::new(); let mut frames = Vec::new(); for frame in self diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index abd1f502699..33a60e7d991 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -253,7 +253,17 @@ impl Capturer { pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result> { if self.output_texture { - Ok(Frame::Texture(self.get_texture(timeout)?)) + let rotation = match self.display.rotation() { + DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0, + DXGI_MODE_ROTATION_ROTATE90 => 90, + DXGI_MODE_ROTATION_ROTATE180 => 180, + DXGI_MODE_ROTATION_ROTATE270 => 270, + _ => { + // Unsupported rotation, try anyway + 0 + } + }; + Ok(Frame::Texture((self.get_texture(timeout)?, rotation))) } else { let width = self.width; let height = self.height; diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 640f37d0b83..2f1e2a85267 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -27,39 +27,40 @@ use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_porta use lazy_static::lazy_static; lazy_static! { - pub static ref RDP_RESPONSE: Mutex> = Mutex::new(None); + pub static ref RDP_SESSION_INFO: Mutex> = Mutex::new(None); } #[inline] pub fn close_session() { - let _ = RDP_RESPONSE.lock().unwrap().take(); + let _ = RDP_SESSION_INFO.lock().unwrap().take(); } #[inline] pub fn is_rdp_session_hold() -> bool { - RDP_RESPONSE.lock().unwrap().is_some() + RDP_SESSION_INFO.lock().unwrap().is_some() } pub fn try_close_session() { - let mut rdp_res = RDP_RESPONSE.lock().unwrap(); + let mut rdp_info = RDP_SESSION_INFO.lock().unwrap(); let mut close = false; - if let Some(rdp_res) = &*rdp_res { + if let Some(rdp_info) = &*rdp_info { // If is server running and restore token is supported, there's no need to keep the session. - if is_server_running() && rdp_res.is_support_restore_token { + if is_server_running() && rdp_info.is_support_restore_token { close = true; } } if close { - *rdp_res = None; + *rdp_info = None; } } -pub struct RdpResponse { +pub struct RdpSessionInfo { pub conn: Arc, pub streams: Vec, pub fd: OwnedFd, pub session: dbus::Path<'static>, pub is_support_restore_token: bool, + pub resolution: Arc>>, } #[derive(Debug, Clone, Copy)] pub struct PwStreamInfo { @@ -69,6 +70,12 @@ pub struct PwStreamInfo { size: (usize, usize), } +impl PwStreamInfo { + pub fn get_size(&self) -> (usize, usize) { + self.size + } +} + #[derive(Debug)] pub struct DBusError(String); @@ -105,24 +112,31 @@ pub struct PipeWireCapturable { } impl PipeWireCapturable { - fn new(conn: Arc, fd: OwnedFd, stream: PwStreamInfo) -> Self { + fn new( + conn: Arc, + fd: OwnedFd, + resolution: Arc>>, + stream: PwStreamInfo, + ) -> Self { // alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling // https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244 - let res = get_res(Self { + let size = get_res(Self { dbus_conn: conn.clone(), fd: fd.clone(), path: stream.path, source_type: stream.source_type, position: stream.position, size: stream.size, - }); + }) + .unwrap_or(stream.size); + *resolution.lock().unwrap() = Some(size); Self { dbus_conn: conn, fd, path: stream.path, source_type: stream.source_type, position: stream.position, - size: res.unwrap_or(stream.size), + size, } } } @@ -813,7 +827,7 @@ fn on_start_response( } pub fn get_capturables() -> Result, Box> { - let mut rdp_connection = match RDP_RESPONSE.lock() { + let mut rdp_connection = match RDP_SESSION_INFO.lock() { Ok(conn) => conn, Err(err) => return Err(Box::new(err)), }; @@ -822,28 +836,36 @@ pub fn get_capturables() -> Result, Box> { let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?; let conn = Arc::new(conn); - let rdp_res = RdpResponse { + let rdp_info = RdpSessionInfo { conn, streams, fd, session, is_support_restore_token, + resolution: Arc::new(Mutex::new(None)), }; - *rdp_connection = Some(rdp_res); + *rdp_connection = Some(rdp_info); } - let rdp_res = match rdp_connection.as_ref() { + let rdp_info = match rdp_connection.as_ref() { Some(res) => res, None => { return Err(Box::new(DBusError("RDP response is None.".into()))); } }; - Ok(rdp_res + Ok(rdp_info .streams .clone() .into_iter() - .map(|s| PipeWireCapturable::new(rdp_res.conn.clone(), rdp_res.fd.clone(), s)) + .map(|s| { + PipeWireCapturable::new( + rdp_info.conn.clone(), + rdp_info.fd.clone(), + rdp_info.resolution.clone(), + s, + ) + }) .collect()) } diff --git a/libs/scrap/src/x11/server.rs b/libs/scrap/src/x11/server.rs index e2ffdc74b4f..f9983f7cf2a 100644 --- a/libs/scrap/src/x11/server.rs +++ b/libs/scrap/src/x11/server.rs @@ -1,3 +1,4 @@ +use hbb_common::libc; use std::ptr; use std::rc::Rc; @@ -99,11 +100,16 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> if reply.is_null() { // TODO: Should seperate SHM disabled from SHM not supported? return Err(Error::UnsupportedExtension); - } else if e.is_null() { - return Ok(()); } else { - // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? - return Err(Error::Generic); + // https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229 + libc::free(reply as *mut _); + if e.is_null() { + return Ok(()); + } else { + libc::free(e as *mut _); + // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? + return Err(Error::Generic); + } } } diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index f68be3c7e36..baef2e2e202 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -14,15 +14,10 @@ case $1 in rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true # workaround temp dev build between 1.1.9 and 1.2.0 - ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) - waylandSupportVersion=21 - if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] + serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) + if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] then - serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] - then - systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true - fi + systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true fi rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true fi diff --git a/res/PKGBUILD b/res/PKGBUILD index 94ccce6937d..616682e8f69 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.0 +pkgver=1.3.2 pkgrel=0 epoch= pkgdesc="" diff --git a/res/com.rustdesk.RustDesk.policy b/res/com.rustdesk.RustDesk.policy deleted file mode 100644 index 55f13629b7f..00000000000 --- a/res/com.rustdesk.RustDesk.policy +++ /dev/null @@ -1,23 +0,0 @@ - - - - RustDesk - https://rustdesk.com/ - rustdesk - - Change RustDesk options - Authentication is required to change RustDesk options - 要更改RustDesk选项, 需要您先通过身份验证 - 要變更RustDesk選項, 需要您先通過身份驗證 - Authentifizierung zum Ändern der RustDesk-Optionen - /usr/share/rustdesk/files/polkit - true - - auth_admin - auth_admin - auth_admin - - - diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 053099c07fa..768b04c28d2 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,9 +1,10 @@ Name: rustdesk -Version: 1.3.0 +Version: 1.3.2 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index f962a2ed1f6..b62c18b3b71 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,9 +1,10 @@ Name: rustdesk -Version: 1.3.0 +Version: 1.3.2 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau libva pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index d84e1481238..1d6a94b131b 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,8 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rpm.spec b/res/rpm.spec index 633c2a220a7..033e95937d2 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,9 +1,10 @@ Name: rustdesk -Version: 1.3.0 +Version: 1.3.2 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base +Requires: gtk3 libxcb libxdo libXfixes alsa-lib libvdpau1 libva2 pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 %description The best open-source remote desktop client software, written in Rust. diff --git a/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch b/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch deleted file mode 100644 index 475fb627f3e..00000000000 --- a/res/vcpkg/ffmpeg/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch +++ /dev/null @@ -1,95 +0,0 @@ -From afe89a70f6bc7ebd0a6a0a31101801b88cbd60ee Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sun, 5 May 2024 12:45:23 +0800 -Subject: [PATCH] use release/7.0's update_bitrate - -Signed-off-by: 21pages ---- - libavcodec/qsvenc.c | 39 +++++++++++++++++++++++++++++++++++++++ - libavcodec/qsvenc.h | 6 ++++++ - 2 files changed, 45 insertions(+) - -diff --git a/libavcodec/qsvenc.c b/libavcodec/qsvenc.c -index 2382c2f5f7..9b34f37eb3 100644 ---- a/libavcodec/qsvenc.c -+++ b/libavcodec/qsvenc.c -@@ -714,6 +714,11 @@ static int init_video_param(AVCodecContext *avctx, QSVEncContext *q) - brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes), - initial_delay_in_kilobytes) + 0x10000) / 0x10000; - -+ q->old_rc_buffer_size = avctx->rc_buffer_size; -+ q->old_rc_initial_buffer_occupancy = avctx->rc_initial_buffer_occupancy; -+ q->old_bit_rate = avctx->bit_rate; -+ q->old_rc_max_rate = avctx->rc_max_rate; -+ - switch (q->param.mfx.RateControlMethod) { - case MFX_RATECONTROL_CBR: - case MFX_RATECONTROL_VBR: -@@ -1657,6 +1662,39 @@ static int update_qp(AVCodecContext *avctx, QSVEncContext *q, - return updated; - } - -+static int update_bitrate(AVCodecContext *avctx, QSVEncContext *q) -+{ -+ int updated = 0; -+ int target_bitrate_kbps, max_bitrate_kbps, brc_param_multiplier; -+ int buffer_size_in_kilobytes, initial_delay_in_kilobytes; -+ -+ UPDATE_PARAM(q->old_rc_buffer_size, avctx->rc_buffer_size); -+ UPDATE_PARAM(q->old_rc_initial_buffer_occupancy, avctx->rc_initial_buffer_occupancy); -+ UPDATE_PARAM(q->old_bit_rate, avctx->bit_rate); -+ UPDATE_PARAM(q->old_rc_max_rate, avctx->rc_max_rate); -+ if (!updated) -+ return 0; -+ -+ buffer_size_in_kilobytes = avctx->rc_buffer_size / 8000; -+ initial_delay_in_kilobytes = avctx->rc_initial_buffer_occupancy / 8000; -+ target_bitrate_kbps = avctx->bit_rate / 1000; -+ max_bitrate_kbps = avctx->rc_max_rate / 1000; -+ brc_param_multiplier = (FFMAX(FFMAX3(target_bitrate_kbps, max_bitrate_kbps, buffer_size_in_kilobytes), -+ initial_delay_in_kilobytes) + 0x10000) / 0x10000; -+ -+ q->param.mfx.BufferSizeInKB = buffer_size_in_kilobytes / brc_param_multiplier; -+ q->param.mfx.InitialDelayInKB = initial_delay_in_kilobytes / brc_param_multiplier; -+ q->param.mfx.TargetKbps = target_bitrate_kbps / brc_param_multiplier; -+ q->param.mfx.MaxKbps = max_bitrate_kbps / brc_param_multiplier; -+ q->param.mfx.BRCParamMultiplier = brc_param_multiplier; -+ av_log(avctx, AV_LOG_VERBOSE, -+ "Reset BufferSizeInKB: %d; InitialDelayInKB: %d; " -+ "TargetKbps: %d; MaxKbps: %d; BRCParamMultiplier: %d\n", -+ q->param.mfx.BufferSizeInKB, q->param.mfx.InitialDelayInKB, -+ q->param.mfx.TargetKbps, q->param.mfx.MaxKbps, q->param.mfx.BRCParamMultiplier); -+ return updated; -+} -+ - static int update_parameters(AVCodecContext *avctx, QSVEncContext *q, - const AVFrame *frame) - { -@@ -1666,6 +1704,7 @@ static int update_parameters(AVCodecContext *avctx, QSVEncContext *q, - return 0; - - needReset = update_qp(avctx, q, frame); -+ needReset |= update_bitrate(avctx, q); - if (!needReset) - return 0; - -diff --git a/libavcodec/qsvenc.h b/libavcodec/qsvenc.h -index b754ac4b56..5745533165 100644 ---- a/libavcodec/qsvenc.h -+++ b/libavcodec/qsvenc.h -@@ -224,6 +224,12 @@ typedef struct QSVEncContext { - int min_qp_p; - int max_qp_b; - int min_qp_b; -+ -+ // These are used for bitrate control reset -+ int old_bit_rate; -+ int old_rc_buffer_size; -+ int old_rc_initial_buffer_occupancy; -+ int old_rc_max_rate; - } QSVEncContext; - - int ff_qsv_enc_init(AVCodecContext *avctx, QSVEncContext *q); --- -2.43.0.windows.1 - diff --git a/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch b/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch deleted file mode 100644 index d46c54af6da..00000000000 --- a/res/vcpkg/ffmpeg/7.0/0001-android-mediacodec-encode-align-64.patch +++ /dev/null @@ -1,40 +0,0 @@ -From be3d9d8092720bbe4239212648d2e9c4ffd7f40c Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Wed, 22 May 2024 17:09:28 +0800 -Subject: [PATCH] android mediacodec encode align 64 - -Signed-off-by: 21pages ---- - libavcodec/mediacodecenc.c | 11 ++++++----- - 1 file changed, 6 insertions(+), 5 deletions(-) - -diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c -index 984014f1b1..8dcd3dcd64 100644 ---- a/libavcodec/mediacodecenc.c -+++ b/libavcodec/mediacodecenc.c -@@ -200,16 +200,17 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) - ff_AMediaFormat_setString(format, "mime", codec_mime); - // Workaround the alignment requirement of mediacodec. We can't do it - // silently for AV_PIX_FMT_MEDIACODEC. -+ const int align = 64; - if (avctx->pix_fmt != AV_PIX_FMT_MEDIACODEC) { -- s->width = FFALIGN(avctx->width, 16); -- s->height = FFALIGN(avctx->height, 16); -+ s->width = FFALIGN(avctx->width, align); -+ s->height = FFALIGN(avctx->height, align); - } else { - s->width = avctx->width; - s->height = avctx->height; -- if (s->width % 16 || s->height % 16) -+ if (s->width % align || s->height % align) - av_log(avctx, AV_LOG_WARNING, -- "Video size %dx%d isn't align to 16, it may have device compatibility issue\n", -- s->width, s->height); -+ "Video size %dx%d isn't align to %d, it may have device compatibility issue\n", -+ s->width, s->height, align); - } - ff_AMediaFormat_setInt32(format, "width", s->width); - ff_AMediaFormat_setInt32(format, "height", s->height); --- -2.34.1 - diff --git a/res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch similarity index 62% rename from res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch rename to res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch index 245a470d39f..5431b3edd05 100644 --- a/res/vcpkg/ffmpeg/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch +++ b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch @@ -1,9 +1,9 @@ -From f0b694749b38b2cfd94df4eed10e667342c234e5 Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sat, 24 Feb 2024 15:33:24 +0800 -Subject: [PATCH 1/2] avcodec/amfenc: add query_timeout option for h264/hevc +From f6988e5424e041ff6f6e241f4d8fa69a04c05e64 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Thu, 5 Sep 2024 16:26:20 +0800 +Subject: [PATCH 1/3] avcodec/amfenc: add query_timeout option for h264/hevc -Signed-off-by: 21pages +Signed-off-by: 21pages --- libavcodec/amfenc.h | 1 + libavcodec/amfenc_h264.c | 4 ++++ @@ -11,10 +11,10 @@ Signed-off-by: 21pages 3 files changed, 9 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 1ab98d2f78..e92120ea39 100644 +index 2dbd378ef8..d636673a9d 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -87,6 +87,7 @@ typedef struct AmfContext { +@@ -89,6 +89,7 @@ typedef struct AmfContext { int quality; int b_frame_delta_qp; int ref_b_frame_delta_qp; @@ -23,40 +23,40 @@ index 1ab98d2f78..e92120ea39 100644 // Dynamic options, can be set after Init() call diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index efb04589f6..f55dbc80f0 100644 +index c1d5f4054e..415828f005 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -121,6 +121,7 @@ static const AVOption options[] = { +@@ -135,6 +135,7 @@ static const AVOption options[] = { { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, - { NULL } - }; -@@ -155,6 +156,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -222,6 +223,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); + if (ctx->query_timeout >= 0) -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { - case FF_PROFILE_H264_BASELINE: + case AV_PROFILE_H264_BASELINE: profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 8ab9330730..7a40bcad31 100644 +index 33a167aa52..65259d7153 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -89,6 +89,7 @@ static const AVOption options[] = { +@@ -98,6 +98,7 @@ static const AVOption options[] = { { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, - { NULL } - }; -@@ -122,6 +123,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -183,6 +184,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate); @@ -64,7 +64,7 @@ index 8ab9330730..7a40bcad31 100644 + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { - case FF_PROFILE_HEVC_MAIN: + case AV_PROFILE_HEVC_MAIN: profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; -- 2.43.0.windows.1 diff --git a/res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch similarity index 71% rename from res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch rename to res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch index 13b055ef289..62b86d08bd6 100644 --- a/res/vcpkg/ffmpeg/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch +++ b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch @@ -1,16 +1,16 @@ -From 4d0d20d96ad458cfec0444b9be0182ca6085ee0c Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Sat, 24 Feb 2024 16:02:44 +0800 -Subject: [PATCH 2/2] libavcodec/amfenc: reconfig when bitrate change +From 6e76c57cf2c0e790228f19c88089eef110fd74aa Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Thu, 5 Sep 2024 16:32:16 +0800 +Subject: [PATCH 2/3] libavcodec/amfenc: reconfig when bitrate change -Signed-off-by: 21pages +Signed-off-by: 21pages --- libavcodec/amfenc.c | 20 ++++++++++++++++++++ libavcodec/amfenc.h | 1 + 2 files changed, 21 insertions(+) diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c -index a033e1220e..3eab01a903 100644 +index 061859f85c..97587fe66b 100644 --- a/libavcodec/amfenc.c +++ b/libavcodec/amfenc.c @@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx) @@ -21,7 +21,7 @@ index a033e1220e..3eab01a903 100644 // configure AMF logger // the return of these functions indicates old state and do not affect behaviour -@@ -575,6 +576,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe +@@ -583,6 +584,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer); } @@ -45,9 +45,9 @@ index a033e1220e..3eab01a903 100644 int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) { AmfContext *ctx = avctx->priv_data; -@@ -586,6 +604,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) - AVFrame *frame = ctx->delayed_frame; - int block_and_wait; +@@ -596,6 +614,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + int query_output_data_flag = 0; + AMF_RESULT res_resubmit; + reconfig_encoder(avctx); + @@ -55,13 +55,13 @@ index a033e1220e..3eab01a903 100644 return AVERROR(EINVAL); diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index e92120ea39..31172645f2 100644 +index d636673a9d..09506ee2e0 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -107,6 +107,7 @@ typedef struct AmfContext { - int me_half_pel; - int me_quarter_pel; - int aud; +@@ -113,6 +113,7 @@ typedef struct AmfContext { + int max_b_frames; + int qvbr_quality_level; + int hw_high_motion_quality_boost; + int64_t av_bitrate; // HEVC - specific options diff --git a/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch b/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch similarity index 88% rename from res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch rename to res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch index 49aef694795..9bcb6e6926c 100644 --- a/res/vcpkg/ffmpeg/5.1/0004-amf-colorspace.patch +++ b/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch @@ -1,32 +1,32 @@ -From 8fd62e4ecd058b09abf8847be5fbbf0eef44a90f Mon Sep 17 00:00:00 2001 +From 14b77216106eaaff9cf701528039ae4264eaf420 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Tue, 16 Jul 2024 14:58:33 +0800 -Subject: [PATCH] amf colorspace +Date: Thu, 5 Sep 2024 16:41:59 +0800 +Subject: [PATCH 3/3] amf colorspace Signed-off-by: 21pages --- libavcodec/amfenc.h | 1 + - libavcodec/amfenc_h264.c | 39 +++++++++++++++++++++++++++++++++ + libavcodec/amfenc_h264.c | 40 ++++++++++++++++++++++++++++++++++ libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 87 insertions(+) + 3 files changed, 88 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 31172645f2..493e01603d 100644 +index 09506ee2e0..7f458b14f7 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -23,6 +23,7 @@ - +@@ -24,6 +24,7 @@ #include #include + #include +#include #include "libavutil/fifo.h" diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index f55dbc80f0..5a6b6e164f 100644 +index 415828f005..7da5a96c71 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -139,6 +139,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) +@@ -200,6 +200,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) AMFRate framerate; AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; @@ -36,7 +36,7 @@ index f55dbc80f0..5a6b6e164f 100644 if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -199,11 +202,47 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) +@@ -266,10 +269,47 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_ASPECT_RATIO, ratio); } @@ -70,25 +70,25 @@ index f55dbc80f0..5a6b6e164f 100644 + color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020; + break; + } -+ } + } + pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; + color_depth = AMF_COLOR_BIT_DEPTH_8; + if (pix_fmt == AV_PIX_FMT_P010) { + color_depth = AMF_COLOR_BIT_DEPTH_10; - } - ++ } ++ + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, color_depth); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, color_profile); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries); + // autodetect rate control method if (ctx->rate_control_mode == AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_UNKNOWN) { - if (ctx->qp_i != -1 || ctx->qp_p != -1 || ctx->qp_b != -1) { diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 7a40bcad31..0260f43c81 100644 +index 65259d7153..7c930d3ccc 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -106,6 +106,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) +@@ -161,6 +161,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) AMFRate framerate; AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; @@ -98,17 +98,17 @@ index 7a40bcad31..0260f43c81 100644 if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -130,6 +133,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) - case FF_PROFILE_HEVC_MAIN: +@@ -191,6 +194,9 @@ FF_ENABLE_DEPRECATION_WARNINGS + case AV_PROFILE_HEVC_MAIN: profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; break; -+ case FF_PROFILE_HEVC_MAIN_10: ++ case AV_PROFILE_HEVC_MAIN_10: + profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10; + break; default: break; } -@@ -158,6 +164,47 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) +@@ -219,6 +225,47 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_ASPECT_RATIO, ratio); } diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index dc35752ff8b..3d4c10906df 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -1,16 +1,8 @@ -if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX) - set(FF_VERSION "n5.1.5") - set(FF_SHA512 "a933f18e53207ccc277b42c9a68db00f31cefec555e6d5d7c57db3409023b2c38fd93ebe2ccfcd17ba2397adb912e93f2388241ca970b7d8bd005ccfe86d5679") -else() - set(FF_VERSION "n7.0.1") - set(FF_SHA512 "1212ebcb78fdaa103b0304373d374e41bf1fe680e1fa4ce0f60624857491c26b4dda004c490c3ef32d4a0e10f42ae6b54546f9f318e2dcfbaa116117f687bc88") -endif() - vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO ffmpeg/ffmpeg - REF "${FF_VERSION}" - SHA512 "${FF_SHA512}" + REF "n${VERSION}" + SHA512 3ba02e8b979c80bf61d55f414bdac2c756578bb36498ed7486151755c6ccf8bd8ff2b8c7afa3c5d1acd862ce48314886a86a105613c05e36601984c334f8f6bf HEAD_REF master PATCHES 0002-fix-msvc-link.patch # upstreamed in future version @@ -18,25 +10,11 @@ vcpkg_from_github( 0005-fix-nasm.patch # upstreamed in future version 0012-Fix-ssl-110-detection.patch 0013-define-WINVER.patch + patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch + patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch + patch/0003-amf-colorspace.patch ) -if(VCPKG_TARGET_IS_WINDOWS OR VCPKG_TARGET_IS_LINUX) - vcpkg_apply_patches( - SOURCE_PATH ${SOURCE_PATH} - PATCHES - ${CMAKE_CURRENT_LIST_DIR}/5.1/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0003-use-release-7.0-s-qsvenc-update_bitrate.patch - ${CMAKE_CURRENT_LIST_DIR}/5.1/0004-amf-colorspace.patch - ) -elseif(VCPKG_TARGET_IS_ANDROID) - vcpkg_apply_patches( - SOURCE_PATH ${SOURCE_PATH} - PATCHES - ${CMAKE_CURRENT_LIST_DIR}/7.0/0001-android-mediacodec-encode-align-64.patch - ) -endif() - if(SOURCE_PATH MATCHES " ") message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces") endif() @@ -130,6 +108,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS) string(APPEND OPTIONS "\ --target-os=win32 \ --toolchain=msvc \ +--cc=cl \ --enable-gpl \ --enable-d3d11va \ --enable-cuda \ @@ -210,6 +189,10 @@ endif() string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"") string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"") +if(VCPKG_TARGET_IS_WINDOWS) + string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") + string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") +endif() # # Setup vcpkg toolchain set(prog_env "") @@ -219,8 +202,9 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER) get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME) set(ENV{CC} "${CC_filename}") string(APPEND OPTIONS " --cc=${CC_filename}") - - # string(APPEND OPTIONS " --host_cc=${CC_filename}") ffmpeg not yet setup for cross builds? + if(VCPKG_HOST_IS_WINDOWS) + string(APPEND OPTIONS " --host_cc=${CC_filename}") + endif() list(APPEND prog_env "${CC_path}") endif() @@ -291,6 +275,13 @@ if(VCPKG_DETECTED_CMAKE_STRIP) list(APPEND prog_env "${STRIP_path}") endif() +if(VCPKG_HOST_IS_WINDOWS) + vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16) + set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe") + list(APPEND prog_env "${MSYS_ROOT}/usr/bin" "${MSYS_ROOT}/usr/share/automake-1.16") +else() + # find_program(SHELL bash) +endif() list(REMOVE_DUPLICATES prog_env) vcpkg_add_to_path(PREPEND ${prog_env}) diff --git a/res/vcpkg/ffmpeg/vcpkg.json b/res/vcpkg/ffmpeg/vcpkg.json index 61ff2c8b549..f7612d9281c 100644 --- a/res/vcpkg/ffmpeg/vcpkg.json +++ b/res/vcpkg/ffmpeg/vcpkg.json @@ -1,6 +1,6 @@ { "name": "ffmpeg", - "version": "7.0.1", + "version": "7.0.2", "port-version": 0, "description": [ "a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.", diff --git a/src/client.rs b/src/client.rs index c4843e484cf..032411e6eb2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use ringbuf::{ring_buffer::RbBase, Rb}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ collections::HashMap, @@ -30,7 +31,6 @@ pub use file_trait::FileManager; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::UnboundedSender; -use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -54,11 +54,15 @@ use hbb_common::{ }, AddrMangle, ResultType, Stream, }; +use hbb_common::{ + config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING, + tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}, +}; pub use helper::*; use scrap::{ codec::Decoder, record::{Recorder, RecorderContext}, - CodecFormat, ImageFormat, ImageRgb, + CodecFormat, ImageFormat, ImageRgb, ImageTexture, }; use crate::{ @@ -84,7 +88,7 @@ pub mod io_loop; pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); pub const VIDEO_QUEUE_SIZE: usize = 120; -const MAX_DECODE_FAIL_COUNTER: usize = 10; // Currently, failed decode cause refresh_video, so make it small +const MAX_DECODE_FAIL_COUNTER: usize = 3; #[cfg(target_os = "linux")] pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; @@ -1150,11 +1154,12 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, pub rgb: ImageRgb, - pub texture: *mut c_void, + pub texture: ImageTexture, recorder: Arc>>, record: bool, _display: usize, // useful for debug fail_counter: usize, + first_frame: bool, } impl VideoHandler { @@ -1175,11 +1180,12 @@ impl VideoHandler { VideoHandler { decoder: Decoder::new(format, luid), rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()), - texture: std::ptr::null_mut(), + texture: Default::default(), recorder: Default::default(), record: false, _display, fail_counter: 0, + first_frame: true, } } @@ -1208,15 +1214,28 @@ impl VideoHandler { self.fail_counter = 0; } else { if self.fail_counter < usize::MAX { - self.fail_counter += 1 + if self.first_frame && self.fail_counter < MAX_DECODE_FAIL_COUNTER { + log::error!("decode first frame failed"); + self.fail_counter = MAX_DECODE_FAIL_COUNTER; + } else { + self.fail_counter += 1; + } + log::error!( + "Failed to handle video frame, fail counter: {}", + self.fail_counter + ); } } + self.first_frame = false; if self.record { - self.recorder - .lock() - .unwrap() - .as_mut() - .map(|r| r.write_frame(frame)); + self.recorder.lock().unwrap().as_mut().map(|r| { + let (w, h) = if *pixelbuffer { + (self.rgb.w, self.rgb.h) + } else { + (self.texture.w, self.texture.h) + }; + r.write_frame(frame, w, h).ok(); + }); } res } @@ -1226,26 +1245,28 @@ impl VideoHandler { /// Reset the decoder, change format if it is Some pub fn reset(&mut self, format: Option) { + log::info!( + "reset video handler for display #{}, format: {format:?}", + self._display + ); #[cfg(target_os = "macos")] self.rgb.set_align(crate::get_dst_align_rgba()); let luid = Self::get_adapter_luid(); let format = format.unwrap_or(self.decoder.format()); self.decoder = Decoder::new(format, luid); self.fail_counter = 0; + self.first_frame = true; } /// Start or stop screen record. - pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) { + pub fn record_screen(&mut self, start: bool, id: String, display: usize) { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { server: false, id, dir: crate::ui_interface::video_save_directory(false), - filename: "".to_owned(), - width: w as _, - height: h as _, - format: scrap::CodecFormat::VP9, + display, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); @@ -1258,7 +1279,7 @@ impl VideoHandler { } // The source of sent password -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] enum PasswordSource { PersonalAb(Vec), SharedAb(String), @@ -1304,6 +1325,13 @@ impl PasswordSource { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ConnToken { + password: Vec, + password_source: PasswordSource, + session_id: u64, +} + /// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { @@ -1334,6 +1362,7 @@ pub struct LoginConfigHandler { password_source: PasswordSource, // where the sent password comes from shared_password: Option, // Store the shared password pub enable_trusted_devices: bool, + pub record: bool, } impl Deref for LoginConfigHandler { @@ -1359,6 +1388,7 @@ impl LoginConfigHandler { mut force_relay: bool, adapter_luid: Option, shared_password: Option, + conn_token: Option, ) { let mut id = id; if id.contains("@") { @@ -1402,10 +1432,22 @@ impl LoginConfigHandler { let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; - let mut sid = rand::random(); + + let conn_token = conn_token + .map(|x| serde_json::from_str::(&x).ok()) + .flatten(); + let mut sid = 0; + if let Some(token) = conn_token { + sid = token.session_id; + self.password = token.password; // use as last password + self.password_source = token.password_source; + } if sid == 0 { - // you won the lottery - sid = 1; + sid = rand::random(); + if sid == 0 { + // you won the lottery + sid = 1; + } } self.session_id = sid; self.supported_encoding = Default::default(); @@ -1425,6 +1467,7 @@ impl LoginConfigHandler { self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; self.shared_password = shared_password; + self.record = LocalConfig::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING); } /// Check if the client should auto login. @@ -1787,28 +1830,6 @@ impl LoginConfigHandler { ) } - pub fn get_option_message_after_login(&self) -> Option { - if self.conn_type.eq(&ConnType::FILE_TRANSFER) - || self.conn_type.eq(&ConnType::PORT_FORWARD) - || self.conn_type.eq(&ConnType::RDP) - { - return None; - } - let mut n = 0; - let mut msg = OptionMessage::new(); - if self.version < hbb_common::get_version_number("1.2.4") { - if self.get_toggle_option("privacy-mode") { - msg.privacy_mode = BoolOption::Yes.into(); - n += 1; - } - } - if n > 0 { - Some(msg) - } else { - None - } - } - /// Parse the image quality option. /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. /// @@ -2227,6 +2248,18 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn get_conn_token(&self) -> Option { + if self.password.is_empty() { + return None; + } + serde_json::to_string(&ConnToken { + password: self.password.clone(), + password_source: self.password_source.clone(), + session_id: self.session_id, + }) + .ok() + } } /// Media data. @@ -2236,7 +2269,7 @@ pub enum MediaData { AudioFrame(Box), AudioFormat(AudioFormat), Reset(Option), - RecordScreen(bool, usize, i32, i32, String), + RecordScreen(bool), } pub type MediaSender = mpsc::Sender; @@ -2312,10 +2345,16 @@ where let start = std::time::Instant::now(); let format = CodecFormat::from(&vf); if !handler_controller_map.contains_key(&display) { + let mut handler = VideoHandler::new(format, display); + let record = session.lc.read().unwrap().record; + let id = session.lc.read().unwrap().id.clone(); + if record { + handler.record_screen(record, id, display); + } handler_controller_map.insert( display, VideoHandlerController { - handler: VideoHandler::new(format, display), + handler, skip_beginning: 0, }, ); @@ -2334,7 +2373,7 @@ where video_callback( display, &mut handler_controller.handler.rgb, - handler_controller.handler.texture, + handler_controller.handler.texture.texture, pixelbuffer, ); @@ -2408,18 +2447,19 @@ where } } } - MediaData::RecordScreen(start, display, w, h, id) => { - log::info!("record screen command: start: {start}, display: {display}"); - // Compatible with the sciter version(single ui session). - // For the sciter version, there're no multi-ui-sessions for one connection. - // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. - if let Some(handler_controler) = handler_controller_map.get_mut(&display) { - handler_controler.handler.record_screen(start, w, h, id); - } else if handler_controller_map.len() == 1 { - if let Some(handler_controler) = - handler_controller_map.values_mut().next() - { - handler_controler.handler.record_screen(start, w, h, id); + MediaData::RecordScreen(start) => { + log::info!("record screen command: start: {start}"); + let record = session.lc.read().unwrap().record; + session.update_record_status(start); + if record != start { + session.lc.write().unwrap().record = start; + let id = session.lc.read().unwrap().id.clone(); + for (display, handler_controler) in handler_controller_map.iter_mut() { + handler_controler.handler.record_screen( + start, + id.clone(), + *display, + ); } } } @@ -3178,7 +3218,7 @@ pub enum Data { SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), - RecordScreen(bool, usize, i32, i32, String), + RecordScreen(bool), ElevateDirect, ElevateWithLogon(String, String), NewVoiceCall, @@ -3411,3 +3451,135 @@ async fn hc_connection_( } Ok(()) } + +pub mod peer_online { + use hbb_common::{ + anyhow::bail, + config::{Config, CONNECT_TIMEOUT, READ_TIMEOUT}, + log, + rendezvous_proto::*, + sleep, + socket_client::connect_tcp, + tcp::FramedStream, + ResultType, + }; + + pub async fn query_online_states, Vec)>(ids: Vec, f: F) { + let test = false; + if test { + sleep(1.5).await; + let mut onlines = ids; + let offlines = onlines.drain((onlines.len() / 2)..).collect(); + f(onlines, offlines) + } else { + let query_timeout = std::time::Duration::from_millis(3_000); + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); + } + Err(e) => { + log::debug!("query onlines, {}", &e); + } + } + } + } + + async fn create_online_stream() -> ResultType { + let (rendezvous_server, _servers, _contained) = + crate::get_rendezvous_server(READ_TIMEOUT).await; + let tmp: Vec<&str> = rendezvous_server.split(":").collect(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", rendezvous_server); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", rendezvous_server); + } + let online_server = format!("{}:{}", tmp[0], port - 1); + connect_tcp(online_server, CONNECT_TIMEOUT).await + } + + async fn query_online_states_( + ids: &Vec, + timeout: std::time::Duration, + ) -> ResultType<(Vec, Vec)> { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_request(OnlineRequest { + id: Config::get_id(), + peers: ids.clone(), + ..Default::default() + }); + + let mut socket = match create_online_stream().await { + Ok(s) => s, + Err(e) => { + log::debug!("Failed to create peers online stream, {e}"); + return Ok((vec![], ids.clone())); + } + }; + // TODO: Use long connections to avoid socket creation + // If we use a Arc>> to hold and reuse the previous socket, + // we may face the following error: + // An established connection was aborted by the software in your host machine. (os error 10053) + if let Err(e) = socket.send(&msg_out).await { + log::debug!("Failed to send peers online states query, {e}"); + return Ok((vec![], ids.clone())); + } + // Retry for 2 times to get the online response + for _ in 0..2 { + if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg( + &mut socket, + Some(timeout.as_millis() as _), + ) + .await + { + match msg_in.union { + Some(rendezvous_message::Union::OnlineResponse(online_response)) => { + let states = online_response.states; + let mut onlines = Vec::new(); + let mut offlines = Vec::new(); + for i in 0..ids.len() { + // bytes index from left to right + let bit_value = 0x01 << (7 - i % 8); + if (states[i / 8] & bit_value) == bit_value { + onlines.push(ids[i].clone()); + } else { + offlines.push(ids[i].clone()); + } + } + return Ok((onlines, offlines)); + } + _ => { + // ignore + } + } + } else { + // TODO: Make sure socket closed? + bail!("Online stream receives None"); + } + } + + bail!("Failed to query online states, no online response"); + } + + #[cfg(test)] + mod tests { + use hbb_common::tokio; + + #[tokio::test] + async fn test_query_onlines() { + super::query_online_states( + vec![ + "152183996".to_owned(), + "165782066".to_owned(), + "155323351".to_owned(), + "460952777".to_owned(), + ], + |onlines: Vec, offlines: Vec| { + println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); + }, + ) + .await; + } + } +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b222e411815..cc74c96edd1 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -26,7 +26,7 @@ use crossbeam_queue::ArrayQueue; use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ allow_err, - config::{PeerConfig, TransferSerde}, + config::{self, PeerConfig, TransferSerde}, fs::{ self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, @@ -353,6 +353,7 @@ impl Remote { } else { if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); + // to-do: Show msgbox with "Don't show again" option }; log::debug!("Send system clipboard message to remote"); let msg = crate::clipboard_file::clip_2_msg(clip); @@ -836,10 +837,8 @@ impl Remote { self.handle_job_status(id, -1, err); } } - Data::RecordScreen(start, display, w, h, id) => { - let _ = self - .video_sender - .send(MediaData::RecordScreen(start, display, w, h, id)); + Data::RecordScreen(start) => { + let _ = self.video_sender.send(MediaData::RecordScreen(start)); } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -957,22 +956,6 @@ impl Remote { true } - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { if !self.peer_info.is_support_virtual_display() { return; @@ -1134,7 +1117,6 @@ impl Remote { self.first_frame = true; self.handler.close_success(); self.handler.adapt_size(); - self.send_opts_after_login(peer).await; self.send_toggle_virtual_display_msg(peer).await; self.send_toggle_privacy_mode_msg(peer).await; } @@ -1212,18 +1194,20 @@ impl Remote { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( - &peer_version, - &peer_platform, - crate::clipboard::ClipboardSide::Client, - ) { - let sender = self.sender.clone(); - let permission_config = self.handler.get_permission_config(); - tokio::spawn(async move { - if permission_config.is_text_clipboard_required() { - sender.send(Data::Message(msg_out)).ok(); - } - }); + if self.handler.lc.read().unwrap().sync_init_clipboard.v { + if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + tokio::spawn(async move { + if permission_config.is_text_clipboard_required() { + sender.send(Data::Message(msg_out)).ok(); + } + }); + } } // on connection established client @@ -1232,7 +1216,7 @@ impl Remote { crate::plugin::handle_listen_event( crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(), self.handler.get_id(), - ) + ); } if self.handler.is_file_transfer() { @@ -1634,7 +1618,7 @@ impl Remote { }, Some(message::Union::MessageBox(msgbox)) => { let mut link = msgbox.link; - if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) { + if let Some(v) = config::HELPER_URL.get(&link as &str) { link = v.to_string(); } else { log::warn!("Message box ignore link {} for security", &link); @@ -1906,7 +1890,7 @@ impl Remote { return; }; - let is_stopping_allowed = clip.is_stopping_allowed_from_peer(); + let is_stopping_allowed = clip.is_beginning_message(); let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v; let stop = is_stopping_allowed && !file_transfer_enabled; log::debug!( diff --git a/src/clipboard.rs b/src/clipboard.rs index 0510eca6a2d..329b392bba7 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,9 +1,10 @@ use arboard::{ClipboardData, ClipboardFormat}; use clipboard_master::{ClipboardHandler, Master, Shutdown}; -use hbb_common::{log, message_proto::*, ResultType}; +use hbb_common::{bail, log, message_proto::*, ResultType}; use std::{ sync::{mpsc::Sender, Arc, Mutex}, thread::JoinHandle, + time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -12,6 +13,9 @@ pub const CLIPBOARD_INTERVAL: u64 = 333; // This format is used to store the flag in the clipboard. const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; +// Add special format for Excel XML Spreadsheet +const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; + lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg @@ -23,6 +27,9 @@ lazy_static::lazy_static! { static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } +const CLIPBOARD_GET_MAX_RETRY: usize = 3; +const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33); + const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::Text, ClipboardFormat::Html, @@ -30,6 +37,7 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::ImageRgba, ClipboardFormat::ImagePng, ClipboardFormat::ImageSvg, + ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET), ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), ]; @@ -147,14 +155,18 @@ pub fn check_clipboard( *ctx = ClipboardContext::new().ok(); } let ctx2 = ctx.as_mut()?; - let content = ctx2.get(side, force); - if let Ok(content) = content { - if !content.is_empty() { - let mut msg = Message::new(); - let clipboards = proto::create_multi_clipboards(content); - msg.set_multi_clipboards(clipboards.clone()); - *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; - return Some(msg); + match ctx2.get(side, force) { + Ok(content) => { + if !content.is_empty() { + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + return Some(msg); + } + } + Err(e) => { + log::error!("Failed to get clipboard content. {}", e); } } None @@ -259,16 +271,49 @@ impl ClipboardContext { Ok(ClipboardContext { inner: board }) } + fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType> { + // If there're multiple threads or processes trying to access the clipboard at the same time, + // the previous clipboard owner will fail to access the clipboard. + // `GetLastError()` will return `ERROR_CLIPBOARD_NOT_OPEN` (OSError(1418): Thread does not have a clipboard open) at this time. + // See https://github.com/rustdesk-org/arboard/blob/747ab2d9b40a5c9c5102051cf3b0bb38b4845e60/src/platform/windows.rs#L34 + // + // This is a common case on Windows, so we retry here. + // Related issues: + // https://github.com/rustdesk/rustdesk/issues/9263 + // https://github.com/rustdesk/rustdesk/issues/9222#issuecomment-2329233175 + for i in 0..CLIPBOARD_GET_MAX_RETRY { + match self.inner.get_formats(SUPPORTED_FORMATS) { + Ok(data) => { + return Ok(data + .into_iter() + .filter(|c| !matches!(c, arboard::ClipboardData::None)) + .collect()) + } + Err(e) => match e { + arboard::Error::ClipboardOccupied => { + log::debug!("Failed to get clipboard formats, clipboard is occupied, retrying... {}", i + 1); + std::thread::sleep(CLIPBOARD_GET_RETRY_INTERVAL_DUR); + } + _ => { + log::error!("Failed to get clipboard formats, {}", e); + return Err(e.into()); + } + }, + } + } + bail!("Failed to get clipboard formats, clipboard is occupied, {CLIPBOARD_GET_MAX_RETRY} retries failed"); + } + pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { let _lock = ARBOARD_MTX.lock().unwrap(); - let data = self.inner.get_formats(SUPPORTED_FORMATS)?; + let data = self.get_formats(SUPPORTED_FORMATS)?; if data.is_empty() { return Ok(data); } if !force { for c in data.iter() { - if let ClipboardData::Special((_, d)) = c { - if side.is_owner(d) { + if let ClipboardData::Special((s, d)) = c { + if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) { return Ok(vec![]); } } @@ -276,7 +321,10 @@ impl ClipboardContext { } Ok(data .into_iter() - .filter(|c| !matches!(c, ClipboardData::Special(_))) + .filter(|c| match c { + ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + _ => true, + }) .collect()) } @@ -454,12 +502,30 @@ mod proto { } } + fn special_to_proto(d: Vec, s: String) -> Clipboard { + let compressed = compress_func(&d); + let compress = compressed.len() < d.len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::Special.into(), + special_name: s, + ..Default::default() + } + } + fn clipboard_data_to_proto(data: ClipboardData) -> Option { let d = match data { ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), ClipboardData::Image(a) => image_to_proto(a), + ClipboardData::Special((s, d)) => special_to_proto(d, s), _ => return None, }; Some(d) @@ -496,6 +562,9 @@ mod proto { Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( std::str::from_utf8(&data).unwrap_or_default(), ))), + Ok(ClipboardFormat::Special) => { + Some(ClipboardData::Special((clipboard.special_name, data))) + } _ => None, } } diff --git a/src/common.rs b/src/common.rs index b6d6c1dd8e9..b213201540e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -84,6 +84,7 @@ lazy_static::lazy_static! { // Is server logic running. The server code can invoked to run by the main process if --server is not running. static ref SERVER_RUNNING: Arc> = Default::default(); static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--")); + static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned()) || std::env::args().nth(1) == Some("--cm-no-ui".to_owned()); } pub struct SimpleCallOnReturn { @@ -137,6 +138,11 @@ pub fn is_main() -> bool { *IS_MAIN } +#[inline] +pub fn is_cm() -> bool { + *IS_CM +} + // Is server logic running. #[inline] pub fn is_server_running() -> bool { @@ -822,7 +828,16 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { let response_url = latest_release_response.url().to_string(); if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { - *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; + #[cfg(feature = "flutter")] + { + let mut m = HashMap::new(); + m.insert("name", "check_software_update_finish"); + m.insert("url", &response_url); + if let Ok(data) = serde_json::to_string(&m) { + let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); + } + } + *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; } Ok(()) } @@ -1644,3 +1659,13 @@ mod tests { ); } } + +#[inline] +pub fn get_builtin_option(key: &str) -> String { + config::BUILTIN_SETTINGS + .read() + .unwrap() + .get(key) + .cloned() + .unwrap_or_default() +} diff --git a/src/core_main.rs b/src/core_main.rs index 3aa69f8f367..23d7706d473 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -39,6 +39,7 @@ pub fn core_main() -> Option> { let mut _is_run_as_system = false; let mut _is_quick_support = false; let mut _is_flutter_invoke_new_connection = false; + let mut no_server = false; let mut arg_exe = Default::default(); for arg in std::env::args() { if i == 0 { @@ -62,6 +63,8 @@ pub fn core_main() -> Option> { _is_run_as_system = true; } else if arg == "--quick_support" { _is_quick_support = true; + } else if arg == "--no-server" { + no_server = true; } else { args.push(arg); } @@ -134,6 +137,7 @@ pub fn core_main() -> Option> { } } hbb_common::init_log(false, &log_name); + log::info!("main start args: {:?}, env: {:?}", args, std::env::args()); // linux uni (url) go here. #[cfg(all(target_os = "linux", feature = "flutter"))] @@ -161,9 +165,8 @@ pub fn core_main() -> Option> { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] init_plugins(&args); - log::info!("main start args:{:?}", args); if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { - std::thread::spawn(move || crate::start_server(false)); + std::thread::spawn(move || crate::start_server(false, no_server)); } else { #[cfg(windows)] { @@ -279,11 +282,11 @@ pub fn core_main() -> Option> { crate::privacy_mode::restore_reg_connectivity(true); #[cfg(any(target_os = "linux", target_os = "windows"))] { - crate::start_server(true); + crate::start_server(true, false); } #[cfg(target_os = "macos")] { - let handler = std::thread::spawn(move || crate::start_server(true)); + let handler = std::thread::spawn(move || crate::start_server(true, false)); crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); @@ -473,8 +476,18 @@ pub fn core_main() -> Option> { crate::ui_interface::start_option_status_sync(); } else if args[0] == "--cm-no-ui" { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))] - crate::flutter::connection_manager::start_cm_no_ui(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::ui_interface::start_option_status_sync(); + crate::flutter::connection_manager::start_cm_no_ui(); + } + return None; + } else if args[0] == "-gtk-sudo" { + // rustdesk service kill `rustdesk --` processes + #[cfg(target_os = "linux")] + if args.len() > 2 { + crate::platform::gtk_sudo::exec(); + } return None; } else { #[cfg(all(feature = "flutter", feature = "plugin_framework"))] diff --git a/src/flutter.rs b/src/flutter.rs index d408202a96d..a1c9c7e3441 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -17,7 +17,7 @@ use serde::Serialize; use serde_json::json; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ffi::CString, os::raw::{c_char, c_int, c_void}, str::FromStr, @@ -802,13 +802,13 @@ impl InvokeUiSession for FlutterHandler { fn set_peer_info(&self, pi: &PeerInfo) { let displays = Self::make_displays_msg(&pi.displays); - let mut features: HashMap<&str, i32> = Default::default(); + let mut features: HashMap<&str, bool> = Default::default(); for ref f in pi.features.iter() { - features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); + features.insert("privacy_mode", f.privacy_mode); } // compatible with 1.1.9 if get_version_number(&pi.version) < get_version_number("1.2.0") { - features.insert("privacy_mode", 0); + features.insert("privacy_mode", false); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); let resolutions = serialize_resolutions(&pi.resolutions.resolutions); @@ -1010,6 +1010,10 @@ impl InvokeUiSession for FlutterHandler { rgba_data.valid = false; } } + + fn update_record_status(&self, start: bool) { + self.push_event("record_status", &[("start", &start.to_string())], &[]); + } } impl FlutterHandler { @@ -1122,6 +1126,7 @@ pub fn session_add( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER @@ -1176,6 +1181,7 @@ pub fn session_add( force_relay, get_adapter_luid(), shared_password, + conn_token, ); let session = Arc::new(session.clone()); @@ -1830,7 +1836,6 @@ pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i3 // sessions mod is used to avoid the big lock of sessions' map. pub mod sessions { - use std::collections::HashSet; use super::*; @@ -2057,18 +2062,18 @@ pub mod sessions { pub(super) mod async_tasks { use hbb_common::{ bail, - tokio::{ - self, select, - sync::mpsc::{unbounded_channel, UnboundedSender}, - }, + tokio::{self, select}, ResultType, }; use std::{ collections::HashMap, - sync::{Arc, Mutex}, + sync::{ + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, }; - type TxQueryOnlines = UnboundedSender>; + type TxQueryOnlines = SyncSender>; lazy_static::lazy_static! { static ref TX_QUERY_ONLINES: Arc>> = Default::default(); } @@ -2085,21 +2090,18 @@ pub(super) mod async_tasks { #[tokio::main(flavor = "current_thread")] async fn start_flutter_async_runner_() { - let (tx_onlines, mut rx_onlines) = unbounded_channel::>(); + // Only one task is allowed to run at the same time. + let (tx_onlines, rx_onlines) = sync_channel::>(1); TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines); loop { - select! { - ids = rx_onlines.recv() => { - match ids { - Some(_ids) => { - #[cfg(not(any(target_os = "ios")))] - crate::rendezvous_mediator::query_online_states(_ids, handle_query_onlines).await - } - None => { - break; - } - } + match rx_onlines.recv() { + Ok(ids) => { + crate::client::peer_online::query_online_states(ids, handle_query_onlines).await + } + _ => { + // unreachable! + break; } } } @@ -2107,7 +2109,8 @@ pub(super) mod async_tasks { pub fn query_onlines(ids: Vec) -> ResultType<()> { if let Some(tx) = TX_QUERY_ONLINES.lock().unwrap().as_ref() { - let _ = tx.send(ids)?; + // Ignore if the channel is full. + let _ = tx.try_send(ids)?; } else { bail!("No tx_query_onlines"); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9b9914cfd0f..7a0c5e87449 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,6 @@ use crate::{ client::file_trait::FileManager, - common::{is_keyboard_mode_supported, make_fd_to_json}, + common::make_fd_to_json, flutter::{ self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, }, @@ -19,13 +19,11 @@ use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, - message_proto::KeyboardMode, rendezvous_proto::ConnType, ResultType, }; use std::{ collections::HashMap, - str::FromStr, sync::{ atomic::{AtomicI32, Ordering}, Arc, @@ -63,7 +61,6 @@ fn initialize(app_dir: &str, custom_client_config: &str) { scrap::mediacodec::check_mediacodec(); crate::common::test_rendezvous_server(); crate::common::test_nat_type(); - crate::common::check_software_update(); } #[cfg(target_os = "ios")] { @@ -124,6 +121,7 @@ pub fn session_add_sync( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> SyncReturn { if let Err(e) = session_add( &session_id, @@ -135,6 +133,7 @@ pub fn session_add_sync( force_relay, password, is_shared_password, + conn_token, ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -225,6 +224,10 @@ pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn SyncReturn { } } -pub fn session_record_screen( - session_id: SessionID, - start: bool, - display: usize, - width: usize, - height: usize, -) { +pub fn session_record_screen(session_id: SessionID, start: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.record_screen(start, display as _, width as _, height as _); + session.record_screen(start); } } -pub fn session_record_status(session_id: SessionID, status: bool) { +pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.record_status(status); + SyncReturn(session.is_recording()) + } else { + SyncReturn(false) } } @@ -447,15 +446,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { - SyncReturn(is_keyboard_mode_supported( - &mode, - session.get_peer_version(), - &session.peer_platform(), - )) - } else { - SyncReturn(false) - } + SyncReturn(session.is_keyboard_mode_supported(mode)) } else { SyncReturn(false) } @@ -490,6 +481,25 @@ pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Ve } pub fn session_handle_flutter_key_event( + session_id: SessionID, + character: String, + usb_hid: i32, + lock_modes: i32, + down_or_up: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + let keyboard_mode = session.get_keyboard_mode(); + session.handle_flutter_key_event( + &keyboard_mode, + &character, + usb_hid, + lock_modes, + down_or_up, + ); + } +} + +pub fn session_handle_flutter_raw_key_event( session_id: SessionID, name: String, platform_code: i32, @@ -499,7 +509,7 @@ pub fn session_handle_flutter_key_event( ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { let keyboard_mode = session.get_keyboard_mode(); - session.handle_flutter_key_event( + session.handle_flutter_raw_key_event( &keyboard_mode, &name, platform_code, @@ -594,6 +604,7 @@ pub fn session_send_files( file_num: i32, include_hidden: bool, is_remote: bool, + _is_dir: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_files(act_id, path, to, file_num, include_hidden, is_remote); @@ -625,7 +636,7 @@ pub fn session_remove_file( } } -pub fn session_read_dir_recursive( +pub fn session_read_dir_to_remove_recursive( session_id: SessionID, act_id: i32, path: String, @@ -1332,6 +1343,14 @@ pub fn session_close_voice_call(session_id: SessionID) { } } +pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.get_conn_token()) + } else { + SyncReturn(None) + } +} + pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { crate::ui_cm_interface::handle_incoming_voice_call(id, accept); } @@ -1367,11 +1386,10 @@ pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } -pub fn main_get_software_update_url() -> String { +pub fn main_get_software_update_url() { if get_local_option("enable-check-update".to_string()) != "N" { crate::common::check_software_update(); } - crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } pub fn main_get_home_dir() -> String { @@ -2273,6 +2291,10 @@ pub fn main_clear_trusted_devices() { clear_trusted_devices() } +pub fn main_max_encrypt_len() -> SyncReturn { + SyncReturn(max_encrypt_len()) +} + pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.request_init_msgs(display); diff --git a/src/ipc.rs b/src/ipc.rs index 3f093c758e2..81693a73558 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -121,6 +121,7 @@ pub struct ClipboardNonFile { pub height: i32, // message.proto: ClipboardFormat pub format: i32, + pub special_name: String, } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -403,7 +404,8 @@ async fn handle(data: Data, stream: &mut Connection) { { hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0) .await; - crate::run_me::<&str>(vec![]).ok(); + // https://github.com/rustdesk/rustdesk/discussions/9254 + crate::run_me::<&str>(vec!["--no-server"]).ok(); } #[cfg(target_os = "macos")] { @@ -888,7 +890,7 @@ pub async fn set_data(data: &Data) -> ResultType<()> { set_data_async(data).await } -pub async fn set_data_async(data: &Data) -> ResultType<()> { +async fn set_data_async(data: &Data) -> ResultType<()> { let mut c = connect(1000, "").await?; c.send(data).await?; Ok(()) @@ -928,16 +930,23 @@ pub fn set_permanent_password(v: String) -> ResultType<()> { pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> { let v = v.trim().to_owned(); let min_len = 4; - if !v.is_empty() && v.len() < min_len { - let err = if translate { - crate::lang::translate( - "Requires at least {".to_string() + &format!("{min_len}") + "} characters", - ) - } else { - // Sometimes, translated can't show normally in command line - format!("Requires at least {} characters", min_len) - }; - bail!(err); + let max_len = crate::ui_interface::max_encrypt_len(); + let len = v.chars().count(); + if !v.is_empty() { + if len < min_len { + let err = if translate { + crate::lang::translate( + "Requires at least {".to_string() + &format!("{min_len}") + "} characters", + ) + } else { + // Sometimes, translated can't show normally in command line + format!("Requires at least {} characters", min_len) + }; + bail!(err); + } + if len > max_len { + bail!("No more than {max_len} characters"); + } } Config::set_unlock_pin(&v); set_config("unlock-pin", v) diff --git a/src/keyboard.rs b/src/keyboard.rs index 6b4b0988fb2..6c68dfa100f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -4,6 +4,7 @@ use crate::flutter; use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk}; #[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::CUR_SESSION; +use crate::ui_session_interface::{InvokeUiSession, Session}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::{client::get_key_state, common::GrabState}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -34,6 +35,7 @@ const OS_LOWER_ANDROID: &str = "android"; #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +#[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false); @@ -71,6 +73,7 @@ pub mod client { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn change_grab_status(state: GrabState, keyboard_mode: &str) { + #[cfg(feature = "flutter")] if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { return; } @@ -103,16 +106,31 @@ pub mod client { pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option) { let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); - if is_long_press(&event) { return; } - - for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { + let peer = get_peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { send_key_event(&key_event); } } + pub fn process_event_with_session( + keyboard_mode: &str, + event: &Event, + lock_modes: Option, + session: &Session, + ) { + let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); + if is_long_press(&event) { + return; + } + let peer = session.peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { + session.send_key_event(&key_event); + } + } + pub fn get_modifiers_state( alt: bool, ctrl: bool, @@ -169,6 +187,7 @@ pub mod client { } } + #[cfg(target_os = "android")] pub fn map_key_to_control_key(key: &rdev::Key) -> Option { match key { Key::Alt => Some(ControlKey::Alt), @@ -356,7 +375,6 @@ pub fn is_long_press(event: &Event) -> bool { return false; } -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn release_remote_keys(keyboard_mode: &str) { // todo!: client quit suddenly, how to release keys? let to_release = TO_RELEASE.lock().unwrap().clone(); @@ -385,7 +403,6 @@ pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode { } #[inline] -#[cfg(not(any(target_os = "ios")))] pub fn is_modifier(key: &rdev::Key) -> bool { matches!( key, @@ -401,7 +418,6 @@ pub fn is_modifier(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { matches!( key, @@ -424,7 +440,6 @@ pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { matches!( key, @@ -460,7 +475,6 @@ pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { // https://github.com/rustdesk/rustdesk/issues/8599 // We just add these keys as letter keys. #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { matches!( key, @@ -469,7 +483,6 @@ pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_numpad_key(event: &Event) -> bool { matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_numpad_rdev_key(&key)) } @@ -477,12 +490,10 @@ fn is_numpad_key(event: &Event) -> bool { // Check is letter key for lock modes. // Only letter keys need to check and send Lock key state. #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_letter_key_4_lock_modes(event: &Event) -> bool { matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if (is_letter_rdev_key(&key) || is_letter_rdev_key_ex(&key))) } -#[cfg(not(any(target_os = "android", target_os = "ios")))] fn parse_add_lock_modes_modifiers( key_event: &mut KeyEvent, lock_modes: i32, @@ -553,10 +564,13 @@ fn update_modifiers_state(event: &Event) { } pub fn event_to_key_events( + mut peer: String, event: &Event, keyboard_mode: KeyboardMode, _lock_modes: Option, ) -> Vec { + peer.retain(|c| !c.is_whitespace()); + let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -570,16 +584,9 @@ pub fn event_to_key_events( _ => {} } - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - key_event.mode = keyboard_mode.into(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let mut key_events; - #[cfg(any(target_os = "android", target_os = "ios"))] - let key_events; - key_events = match keyboard_mode { + let mut key_events = match keyboard_mode { KeyboardMode::Map => map_keyboard_mode(peer.as_str(), event, key_event), KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { @@ -594,15 +601,14 @@ pub fn event_to_key_events( } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] let is_numpad_key = is_numpad_key(&event); - #[cfg(not(any(target_os = "android", target_os = "ios")))] if keyboard_mode != KeyboardMode::Translate || is_numpad_key { let is_letter_key = is_letter_key_4_lock_modes(&event); for key_event in &mut key_events { if let Some(lock_modes) = _lock_modes { parse_add_lock_modes_modifiers(key_event, lock_modes, is_numpad_key, is_letter_key); } else { + #[cfg(not(any(target_os = "android", target_os = "ios")))] add_lock_modes_modifiers(key_event, is_numpad_key, is_letter_key); } } @@ -615,6 +621,7 @@ pub fn send_key_event(key_event: &KeyEvent) { if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { session.send_key_event(key_event); } + #[cfg(feature = "flutter")] if let Some(session) = flutter::get_cur_session() { session.send_key_event(key_event); @@ -934,8 +941,19 @@ fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Op _ => event.position_code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] - let keycode = 0; - + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::usb_hid_code_to_win_scancode(event.usb_hid as _)?, + OS_LOWER_LINUX => rdev::usb_hid_code_to_linux_code(event.usb_hid as _)?, + OS_LOWER_MACOS => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::usb_hid_code_to_macos_iso_code(event.usb_hid as _)? + } else { + rdev::usb_hid_code_to_macos_code(event.usb_hid as _)? + } + } + OS_LOWER_ANDROID => rdev::usb_hid_code_to_android_key_code(event.usb_hid as _)?, + _ => event.usb_hid as _, + }; key_event.set_chr(keycode as _); Some(key_event) } diff --git a/src/lan.rs b/src/lan.rs index 666e73c7c3d..7d3f4f05fed 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -1,4 +1,3 @@ -#[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::config::Config; use hbb_common::{ allow_err, @@ -22,7 +21,7 @@ use std::{ type Message = RendezvousMessage; -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] pub(super) fn start_listening() -> ResultType<()> { let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); let socket = std::net::UdpSocket::bind(addr)?; @@ -40,13 +39,22 @@ pub(super) fn start_listening() -> ResultType<()> { &Config::get_option("enable-lan-discovery"), ) { + let id = Config::get_id(); + if p.id == id { + continue; + } if let Some(self_addr) = get_ipaddr_by_peer(&addr) { let mut msg_out = Message::new(); + let mut hostname = whoami::hostname(); + // The default hostname is "localhost" which is a bit confusing + if hostname == "localhost" { + hostname = "unknown".to_owned(); + } let peer = PeerDiscovery { cmd: "pong".to_owned(), mac: get_mac(&self_addr), - id: Config::get_id(), - hostname: whoami::hostname(), + id, + hostname, username: crate::platform::get_active_username(), platform: whoami::platform().to_string(), ..Default::default() @@ -100,17 +108,17 @@ fn get_broadcast_port() -> u16 { } fn get_mac(_ip: &IpAddr) -> String { - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(target_os = "ios"))] if let Ok(mac) = get_mac_by_ip(_ip) { mac.to_string() } else { "".to_owned() } - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] "".to_owned() } -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(target_os = "ios"))] fn get_mac_by_ip(ip: &IpAddr) -> ResultType { for interface in default_net::get_interfaces() { match ip { @@ -153,6 +161,10 @@ fn get_ipaddr_by_peer(peer: A) -> Option { fn create_broadcast_sockets() -> Vec { let mut ipv4s = Vec::new(); + // TODO: maybe we should use a better way to get ipv4 addresses. + // But currently, it's ok to use `[Ipv4Addr::UNSPECIFIED]` for discovery. + // `default_net::get_interfaces()` causes undefined symbols error when `flutter build` on iOS simulator x86_64 + #[cfg(not(any(target_os = "ios")))] for interface in default_net::get_interfaces() { for ipv4 in &interface.ipv4 { ipv4s.push(ipv4.addr.clone()); @@ -178,8 +190,20 @@ fn send_query() -> ResultType> { } let mut msg_out = Message::new(); + // We may not be able to get the mac address on mobile platforms. + // So we need to use the id to avoid discovering ourselves. + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = crate::ui_interface::get_id(); + // `crate::ui_interface::get_id()` will cause error: + // `get_id()` uses async code with `current_thread`, which is not allowed in this context. + // + // No need to get id for desktop platforms. + // We can use the mac address to identify the device. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let id = "".to_owned(); let peer = PeerDiscovery { cmd: "ping".to_owned(), + id, ..Default::default() }; msg_out.set_peer_discovery(peer); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 68c041481e5..31fd680fd60 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "عرض مراقب الجودة"), ("Disable clipboard", "تعطيل الحافظة"), ("Lock after session end", "القفل بعد نهاية هذه الجلسة"), - ("Insert", "ادخال"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del دخال"), ("Insert Lock", "قفل الادخال"), ("Refresh", "تحديث"), ("ID does not exist", "المعرف غير موجود"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "التسجيل"), ("Directory", "المسار"), ("Automatically record incoming sessions", "تسجيل الجلسات القادمة تلقائيا"), + ("Automatically record outgoing sessions", ""), ("Change", "تغيير"), ("Start session recording", "بدء تسجيل الجلسة"), ("Stop session recording", "ايقاف تسجيل الجلسة"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 136ccf9feec..fbe16153543 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Паказваць манітор якасці"), ("Disable clipboard", "Адключыць буфер абмену"), ("Lock after session end", "Заблакаваць уліковы запіс пасля сеансу"), - ("Insert", "Уставіць"), + ("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"), ("Insert Lock", "Заблакаваць уліковы запіс"), ("Refresh", "Абнавіць"), ("ID does not exist", "ID не існуе"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запіс"), ("Directory", "Тэчка"), ("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"), + ("Automatically record outgoing sessions", ""), ("Change", "Змяніць"), ("Start session recording", "Пачаць запіс сесіі"), ("Stop session recording", "Спыніць запіс сесіі"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 9a5b320a85d..4f0131cc878 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -1,19 +1,19 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Статус"), - ("Your Desktop", "Твоят Работен Плот"), - ("desk_tip", "Вашият работен плот може да бъде достъпен с този идентификационен код и парола."), + ("Status", "Положение"), + ("Your Desktop", "Вашата работна среда"), + ("desk_tip", "Вашата работна среда не може да бъде достъпена с този потребителски код и парола."), ("Password", "Парола"), ("Ready", "Готово"), ("Established", "Установен"), ("connecting_status", "Свързване с RustDesk мрежата..."), - ("Enable service", "Пусни услуга"), + ("Enable service", "Разреши услуга"), ("Start service", "Стартирай услуга"), ("Service is running", "Услугата работи"), ("Service is not running", "Услугата не работи"), ("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"), - ("Control Remote Desktop", "Контролирайте отдалечения работен плот"), + ("Control Remote Desktop", "Отдалечено управление на работна среда"), ("Transfer file", "Прехвърляне на файл"), ("Connect", "Свързване"), ("Recent sessions", "Последни сесии"), @@ -23,27 +23,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove", "Премахване"), ("Refresh random password", "Опресняване на произволна парола"), ("Set your own password", "Задайте собствена парола"), - ("Enable keyboard/mouse", "Разрешение на клавиатура/мишка"), - ("Enable clipboard", "Разрешение на клипборда"), - ("Enable file transfer", "Разрешение прехвърлянето на файлове"), - ("Enable TCP tunneling", "Разрешение за TCP тунел"), - ("IP Whitelisting", "IP беял списък"), - ("ID/Relay Server", "ID/Релейн сървър"), - ("Import server config", "Експортиране конфигурацията на сървъра"), - ("Export Server Config", "Експортиране на конфигурация на сървъра"), - ("Import server configuration successfully", "Импортирането конфигурацията на сървъра успешно"), - ("Export server configuration successfully", "Експортирането конфигурацията на сървъра успешно"), - ("Invalid server configuration", "Невалидна конфигурация на сървъра"), + ("Enable keyboard/mouse", "Позволяване на клавиатура/мишка"), + ("Enable clipboard", "Позволяване достъп до клипборда"), + ("Enable file transfer", "Позволяване прехвърляне на файлове"), + ("Enable TCP tunneling", "Позволяване на TCP тунели"), + ("IP Whitelisting", "Определяне на позволени IP по списък"), + ("ID/Relay Server", "ID/Препредаващ сървър"), + ("Import server config", "Внасяне сървър настройки за "), + ("Export Server Config", "Изнасяне настройки на сървър"), + ("Import server configuration successfully", "Успешно внасяне на сървърни настройки"), + ("Export server configuration successfully", "Успешно изнасяне на сървърни настройки"), + ("Invalid server configuration", "Недопустими сървърни настройки"), ("Clipboard is empty", "Клипбордът е празен"), - ("Stop service", "Спрете услугата"), - ("Change ID", "Промяна на ID"), - ("Your new ID", "Вашето ново ID"), + ("Stop service", "Спираане на услуга"), + ("Change ID", "Промяна определител (ID)"), + ("Your new ID", "Вашият нов определител (ID)"), ("length %min% to %max%", "дължина %min% до %max%"), ("starts with a letter", "започва с буква"), ("allowed characters", "разрешени знаци"), - ("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) символи са позволени. Първата буква трябва да е a-z, A-Z. С дължина мержу 6 и 16."), + ("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) са сред позволени. Първа буква следва да е a-z, A-Z. С дължина мержу 6 и 16."), ("Website", "Уебсайт"), - ("About", "Относно програмата"), + ("About", "Относно"), ("Slogan_tip", "Направено от сърце в този хаотичен свят!"), ("Privacy Statement", "Декларация за поверителност"), ("Mute", "Без звук"), @@ -53,23 +53,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "Аудио вход"), ("Enhancements", "Подобрения"), ("Hardware Codec", "Хардуерен кодек"), - ("Adaptive bitrate", "Адаптивен битрейт"), + ("Adaptive bitrate", "Приспособяваще се скорост на предаване наданни"), ("ID Server", "ID сървър"), - ("Relay Server", "Релейн сървър"), + ("Relay Server", "Препращащ сървър"), ("API Server", "API сървър"), ("invalid_http", "трябва да започва с http:// или https://"), - ("Invalid IP", "Невалиден IP"), - ("Invalid format", "Невалиден формат"), + ("Invalid IP", "Недопустим IP"), + ("Invalid format", "Недопустим формат"), ("server_not_support", "Все още не се поддържа от сървъра"), ("Not available", "Не е наличен"), ("Too frequent", "Твърде често"), ("Cancel", "Отказ"), ("Skip", "Пропускане"), - ("Close", "Затвори"), - ("Retry", "Опитайте отново"), + ("Close", "Затваряне"), + ("Retry", "Преповтори"), ("OK", "Добре"), ("Password Required", "Изисква се парола"), - ("Please enter your password", "Моля въведете паролата си"), + ("Please enter your password", "Моля въведете парола"), ("Remember password", "Запомни паролата"), ("Wrong Password", "Грешна парола"), ("Do you want to enter again?", "Искате ли да въведете отново?"), @@ -99,73 +99,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Properties", "Свойства"), ("Multi Select", "Множествен избор"), ("Select All", "Избери всички"), - ("Unselect All", "Деселектирай всички"), - ("Empty Directory", "Празна директория"), - ("Not an empty directory", "Не е празна директория"), + ("Unselect All", "Избери никой"), + ("Empty Directory", "Празна папка"), + ("Not an empty directory", "Не е празна папка"), ("Are you sure you want to delete this file?", "Сигурни ли сте, че искате да изтриете този файл?"), - ("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна директория?"), - ("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази директория?"), - ("Do this for all conflicts", "Направете това за всички конфликти"), - ("This is irreversible!", ""), + ("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна папка?"), + ("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази папка?"), + ("Do this for all conflicts", "Разреши така всички конфликти"), + ("This is irreversible!", "Това е необратимо!"), ("Deleting", "Изтриване"), ("files", "файлове"), - ("Waiting", ""), - ("Finished", "Готово"), + ("Waiting", "Изчакване"), + ("Finished", "Завършено"), ("Speed", "Скорост"), - ("Custom Image Quality", "Персонализирано качество на изображението"), + ("Custom Image Quality", "Качество на изображението по свой избор"), ("Privacy mode", "Режим на поверителност"), - ("Block user input", "Блокиране на потребителско въвеждане"), - ("Unblock user input", "Отблокиране на потребителско въвеждане"), - ("Adjust Window", "Регулирай прозореца"), + ("Block user input", "Забрана за потребителски вход"), + ("Unblock user input", "Разрешаване на потребителски въвеждане"), + ("Adjust Window", "Нагласи прозореца"), ("Original", "Оригинално"), ("Shrink", "Свиване"), ("Stretch", "Разтегнат"), ("Scrollbar", "Плъзгач"), - ("ScrollAuto", "Автоматичен плъзгач"), + ("ScrollAuto", "Автоматичено приплъзване"), ("Good image quality", "Добро качество на изображението"), - ("Balanced", "Балансиран"), - ("Optimize reaction time", "Оптимизирайте времето за реакция"), - ("Custom", "Персонализиран"), - ("Show remote cursor", "Показване на дистанционния курсор"), - ("Show quality monitor", "Показване на прозорец за качество"), - ("Disable clipboard", "Деактивиране на клипборда"), - ("Lock after session end", "Заключване след края на сесията"), - ("Insert", "Поставяне"), + ("Balanced", "Уравновесен"), + ("Optimize reaction time", "С оглед времето на реакция"), + ("Custom", "По собствено желание"), + ("Show remote cursor", "Показвай отдалечения курсор"), + ("Show quality monitor", "Показвай прозорец за качество"), + ("Disable clipboard", "Забрана за достъп до клипборд"), + ("Lock after session end", "Заключване след край на ползване"), + ("Insert Ctrl + Alt + Del", "Поставяне Ctrl + Alt + Del"), ("Insert Lock", "Заявка за заключване"), - ("Refresh", "Обнови"), - ("ID does not exist", "ID-то не съществува"), - ("Failed to connect to rendezvous server", "Неуспешно свързване със сървъра за рандеву"), + ("Refresh", "Обновяване"), + ("ID does not exist", "Несъществуващ определител (ID)"), + ("Failed to connect to rendezvous server", "Неуспешно свързване към сървъра за среща (rendezvous)"), ("Please try later", "Моля опитайте по-късно"), - ("Remote desktop is offline", "Отдалеченият работен плот е офлайн"), + ("Remote desktop is offline", "Отдалечената работна среда не е налична"), ("Key mismatch", "Ключово несъответствие"), - ("Timeout", ""), - ("Failed to connect to relay server", ""), - ("Failed to connect via rendezvous server", ""), - ("Failed to connect via relay server", ""), - ("Failed to make direct connection to remote desktop", ""), - ("Set Password", "Задайте парола"), + ("Timeout", "Изтичане на времето"), + ("Failed to connect to relay server", "Провал при свързване към препредаващ сървър"), + ("Failed to connect via rendezvous server", "Провал при свързване към сървър за срещи (rendezvous)"), + ("Failed to connect via relay server", "Провал при свързване чрез препредаващ сървър"), + ("Failed to make direct connection to remote desktop", "Провал при установяване на пряка връзка с отдалечена работна среда"), + ("Set Password", "Задаване на парола"), ("OS Password", "Парола на Операционната система"), - ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно като отдалечена достъп. За да заобиколите UAC, моля, щракнете върху бутона по-долу, за да инсталирате RustDesk в системата."), - ("Click to upgrade", "Кликнете, за да надстроите"), - ("Click to download", "Кликнете, за да изтеглите"), - ("Click to update", "Кликнете, за да актуализирате"), - ("Configure", "Конфигуриране"), - ("config_acc", "За да управлявате вашия работен плот дистанционно, трябва да предоставите на RustDesk разрешения \"Достъпност\"."), - ("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."), - ("Installing ...", "Инсталиране..."), - ("Install", "Инсталирай"), - ("Installation", "Инсталация"), - ("Installation Path", "Инсталационен път"), - ("Create start menu shortcuts", "Създайте преки пътища в менюто 'Старт'."), + ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно за отдалечена достъп. За да заобиколите UAC, моля, натиснете копчето по-долу, за да поставите RustDesk като системна услуга."), + ("Click to upgrade", "Натиснете, за да надстроите"), + ("Click to download", "Натиснете, за да изтеглите"), + ("Click to update", "Натиснете, за да обновите"), + ("Configure", "Настройване"), + ("config_acc", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Достъпност\"."), + ("config_screen", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Запис на екрана\"."), + ("Installing ...", "Поставяне..."), + ("Install", "Постави"), + ("Installation", "Поставяне"), + ("Installation Path", "Път към място за поставяне"), + ("Create start menu shortcuts", "Бърз достъп от меню 'Старт'."), ("Create desktop icon", "Създайте икона на работния плот"), - ("agreement_tip", "Стартирайки инсталацията, вие приемате лицензионното споразумение."), - ("Accept and Install", "Приемете и инсталирайте"), - ("End-user license agreement", ""), - ("Generating ...", "Генериране..."), + ("agreement_tip", "Започвайки поставянето, вие приемате лицензионното споразумение."), + ("Accept and Install", "Приемете и поставяте"), + ("End-user license agreement", "Споразумение с потребителя"), + ("Generating ...", "Пораждане..."), ("Your installation is lower version.", "Вашата инсталация е по-ниска версия."), ("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"), ("Listening ...", "Слушане..."), - ("Remote Host", "Отдалечен хост"), + ("Remote Host", "Отдалечен сървър"), ("Remote Port", "Отдалечен порт"), ("Action", "Действие"), ("Add", "Добави"), @@ -173,154 +173,154 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "Локален адрес"), ("Change Local Port", "Промяна на локалният порт"), ("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), + ("Too short, at least 6 characters.", "Прекалено кратко, поне 6 знака"), + ("The confirmation is not identical.", "Потвърждението не съвпада"), ("Permissions", "Разрешения"), ("Accept", "Приеми"), ("Dismiss", "Отхвърляне"), - ("Disconnect", "Прекъснете връзката"), - ("Enable file copy and paste", ""), + ("Disconnect", "Прекъсване"), + ("Enable file copy and paste", "Разрешаване прехвърляне на файлове"), ("Connected", "Свързан"), - ("Direct and encrypted connection", "Директна и криптирана връзка"), - ("Relayed and encrypted connection", "Препредадена и криптирана връзка"), - ("Direct and unencrypted connection", "Директна и некриптирана връзка"), - ("Relayed and unencrypted connection", "Препредадена и некриптирана връзка"), - ("Enter Remote ID", "Въведете дистанционно ID"), - ("Enter your password", "Въведете паролата си"), - ("Logging in...", ""), - ("Enable RDP session sharing", "Активирайте споделянето на RDP сесия"), + ("Direct and encrypted connection", "Пряка защитена връзка"), + ("Relayed and encrypted connection", "Препредадена защитена връзка"), + ("Direct and unencrypted connection", "Пряка незащитена връзка"), + ("Relayed and unencrypted connection", "Препредадена незащитена връзка"), + ("Enter Remote ID", "Въведете отдалеченото ID"), + ("Enter your password", "Въведете парола"), + ("Logging in...", "Вписване..."), + ("Enable RDP session sharing", "Позволяване споделянето на RDP сесия"), ("Auto Login", "Автоматично вписване (Валидно само ако зададете \"Заключване след края на сесията\")"), - ("Enable direct IP access", "Разрешете директен IP достъп"), + ("Enable direct IP access", "Разрешаване пряк IP достъп"), ("Rename", "Преименуване"), ("Space", "Пространство"), ("Create desktop shortcut", "Създайте пряк път на работния плот"), ("Change Path", "Промяна на пътя"), ("Create Folder", "Създай папка"), - ("Please enter the folder name", "Моля, въведете името на папката"), + ("Please enter the folder name", "Моля, въведете име на папката"), ("Fix it", "Оправи го"), ("Warning", "Внимание"), - ("Login screen using Wayland is not supported", "Екранът за влизане с помощта на Wayland не се поддържа"), - ("Reboot required", "Изисква се рестартиране"), - ("Unsupported display server", "Неподдържан сървър за дисплея"), - ("x11 expected", ""), + ("Login screen using Wayland is not supported", "Екран за влизане чрез Wayland не се поддържа"), + ("Reboot required", "Нужно е презареждане на ОС"), + ("Unsupported display server", "Неподдържан екранен сървър"), + ("x11 expected", "Очаква се x11"), ("Port", "Порт"), ("Settings", "Настройки"), ("Username", "Потребителско име"), - ("Invalid port", "Невалиден порт"), - ("Closed manually by the peer", "Затворено ръчно от партньора"), + ("Invalid port", "Недопустим порт"), + ("Closed manually by the peer", "Затворено ръчно от другата страна"), ("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"), ("Run without install", "Стартирайте без инсталиране"), - ("Connect via relay", "Свържете чрез реле"), - ("Always connect via relay", "Винаги свързвайте чрез реле"), + ("Connect via relay", "Свързване чрез препращане"), + ("Always connect via relay", "Винаги чрез препращане"), ("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"), ("Login", "Влизане"), ("Verify", "Потвърди"), ("Remember me", "Запомни ме"), - ("Trust this device", "Доверете се на това устройство"), + ("Trust this device", "Доверяване на това устройство"), ("Verification code", "Код за потвърждение"), - ("verification_tip", "На регистрирания имейл адрес е изпратен код за потвърждение, въведете кода за потвърждение, за да продължите да влизате."), - ("Logout", "Излез от профила си"), - ("Tags", "Етикети"), - ("Search ID", "Търсене на ID"), - ("whitelist_sep", "Разделени със запетая, точка и запетая, интервали или нов ред"), + ("verification_tip", "На посочения имейл е изпратен код за потвърждение. Моля въведете го, за да продължите с влизането."), + ("Logout", "Отписване (Изход)"), + ("Tags", "Белези"), + ("Search ID", "Търси ID"), + ("whitelist_sep", "Разделени със запетая, точка и запетая, празни символи или нов ред"), ("Add ID", "Добави ID"), ("Add Tag", "Добави етикет"), - ("Unselect all tags", "Премахнете избора на всички етикети"), + ("Unselect all tags", "Премахнете избора на всички белези (tags)"), ("Network error", "Мрежова грешка"), - ("Username missed", "Пропуснато потребителско име"), - ("Password missed", "Пропусната парола"), - ("Wrong credentials", "Wrong username or password"), - ("The verification code is incorrect or has expired", ""), - ("Edit Tag", "Edit tag"), + ("Username missed", "Липсващо потребителско име"), + ("Password missed", "Липсваща парола"), + ("Wrong credentials", "Грешни пълномощия"), + ("The verification code is incorrect or has expired", "Кодът за проверка е неправилен или с изтекла давност."), + ("Edit Tag", "Промени белег"), ("Forget Password", "Забравена парола"), - ("Favorites", ""), + ("Favorites", "Любими"), ("Add to Favorites", "Добави към любими"), ("Remove from Favorites", "Премахване от любими"), ("Empty", "Празно"), - ("Invalid folder name", ""), - ("Socks5 Proxy", "Socks5 прокси"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"), - ("Discovered", ""), - ("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."), - ("Remote ID", "Дистанционно ID"), + ("Invalid folder name", "Непозволено име на папка"), + ("Socks5 Proxy", "Socks5 посредник"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) посредник"), + ("Discovered", "Открит"), + ("install_daemon_tip", "За зареждане при стартиране на ОС следва да поставите RustDesk като системна услуга."), + ("Remote ID", "Отдалечено ID"), ("Paste", "Постави"), ("Paste here?", "Постави тук?"), ("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"), - ("Download new version", ""), - ("Touch mode", "Режим тъч (сензорен)"), + ("Download new version", "Изтегляне на нова версия"), + ("Touch mode", "Режим сензорен (touch)"), ("Mouse mode", "Режим мишка"), - ("One-Finger Tap", "Докосване с един пръст"), + ("One-Finger Tap", "Допир с един пръст"), ("Left Mouse", "Ляв бутон на мишката"), - ("One-Long Tap", "Едно дълго докосване"), - ("Two-Finger Tap", "Докосване с два пръста"), + ("One-Long Tap", "Дълъг допир"), + ("Two-Finger Tap", "Допир с два пръста"), ("Right Mouse", "Десен бутон на мишката"), ("One-Finger Move", "Преместване с един пръст"), - ("Double Tap & Move", "Докоснете два пъти и преместете"), - ("Mouse Drag", "Плъзгане с мишката"), + ("Double Tap & Move", "Двоен допир и преместване"), + ("Mouse Drag", "Провличане с мишката"), ("Three-Finger vertically", "Три пръста вертикално"), ("Mouse Wheel", "Колело на мишката"), ("Two-Finger Move", "Движение с два пръста"), ("Canvas Move", "Преместване на платното"), ("Pinch to Zoom", "Щипнете, за да увеличите"), ("Canvas Zoom", "Увеличение на платното"), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), + ("Reset canvas", "Нулиране на платното"), + ("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"), + ("Note", "Бележка"), + ("Connection", "Връзка"), ("Share Screen", "Сподели екран"), - ("Chat", "Чат"), - ("Total", "Обшо"), - ("items", "елементи"), + ("Chat", "Говор"), + ("Total", "Общо"), + ("items", "неща"), ("Selected", "Избрано"), - ("Screen Capture", "Заснемане на екрана"), - ("Input Control", "Контрол на въвеждане"), - ("Audio Capture", "Аудио записване"), + ("Screen Capture", "Снемане на екрана"), + ("Input Control", "Управление на вход"), + ("Audio Capture", "Аудиозапис"), ("File Connection", "Файлова връзка"), - ("Screen Connection", "Свързване на екрана"), + ("Screen Connection", "Екранна връзка"), ("Do you accept?", "Приемате ли?"), - ("Open System Setting", "Отворете системната настройка"), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или докосване, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), + ("Open System Setting", "Отворете системните настройки"), + ("How to get Android input permission?", "Как да получим право за въвеждане под Андрид?"), + ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или допир, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), ("android_input_permission_tip2", "Моля, отидете на следващата страница с системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."), - ("android_new_connection_tip", "Получена е нова заявка за контрол, която иска да контролира вашето текущо устройство."), - ("android_service_will_start_tip", "Включването на \"Заснемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), + ("android_new_connection_tip", "Получена е нова заявка за отдалечено управление на вашето текущо устройство."), + ("android_service_will_start_tip", "Включването на \"Снемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), ("android_stop_service_tip", "Затварянето на услугата автоматично ще затвори всички установени връзки."), - ("android_version_audio_tip", "Текущата версия на Android не поддържа аудио заснемане, моля, актуализирайте устройството с Android 10 или по-нова версия."), - ("android_start_service_tip", "Докоснете [Start service] или активирайте разрешение [Screen Capture], за да стартирате услугата за споделяне на екрана."), - ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, докато не се свържете отново."), - ("Account", "Акаунт"), + ("android_version_audio_tip", "Текущата версия на Android не поддържа аудиозапис. Моля, актуализирайте устройството с Android 10 или по-нов."), + ("android_start_service_tip", "Докоснете [Start service] или позволете [Screen Capture], за да започне услугата по споделяне на екрана."), + ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, а ще изискват да се свържете отново."), + ("Account", "Сметка"), ("Overwrite", "Презаписване"), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", "Излез"), + ("This file exists, skip or overwrite this file?", "Този файл съществува вече. Пропускане или презаписване?"), + ("Quit", "Изход"), ("Help", "Помощ"), ("Failed", "Неуспешно"), ("Succeeded", "Успешно"), - ("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, излезте"), - ("Unsupported", "Не се поддържа"), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), + ("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, изход"), + ("Unsupported", "Неподдържан"), + ("Peer denied", "Отказ от другата страна"), + ("Please install plugins", "Моля поставете приставки"), + ("Peer exit", "Изход от другата страна"), + ("Failed to turn off", "Провал при опит за изключване"), + ("Turned off", "Изкключен"), ("Language", "Език"), - ("Keep RustDesk background service", ""), + ("Keep RustDesk background service", "Запази работеща фонова услуга с RustDesk"), ("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"), ("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"), ("Start on boot", "Стартирайте при зареждане"), ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", "Режим на превод"), - ("Use permanent password", "Използвайте постоянна парола"), - ("Use both passwords", "Използвайте и двете пароли"), - ("Set permanent password", "Задайте постоянна парола"), - ("Enable remote restart", "Разрешете отдалечено рестартиране"), - ("Restart remote device", "Рестартирайте отдалеченото устройство"), + ("Connection not allowed", "Връзката непозволена"), + ("Legacy mode", "По остарял начин"), + ("Map mode", "По начин със съответствие (map)"), + ("Translate mode", "По нчаин с превод"), + ("Use permanent password", "Използване на постоянна парола"), + ("Use both passwords", "Използване и на двете пароли"), + ("Set permanent password", "Задаване постоянна парола"), + ("Enable remote restart", "Разрешаване на отдалечен рестарт"), + ("Restart remote device", "Рестартиране на отдалечено устройство"), ("Are you sure you want to restart", "Сигурни ли сте, че искате да рестартирате"), - ("Restarting remote device", "Рестартира се отдалечено устройство"), + ("Restarting remote device", "Рестартиране на отдалечено устройство"), ("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"), - ("Copied", "Копирано"), + ("Copied", "Преписано"), ("Exit Fullscreen", "Изход от цял екран"), ("Fullscreen", "Цял екран"), ("Mobile Actions", "Мобилни действия"), @@ -334,10 +334,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide Toolbar", "Скриване на лентата с инструменти"), ("Direct Connection", "Директна връзка"), ("Relay Connection", "Релейна връзка"), - ("Secure Connection", "Защитена връзка"), - ("Insecure Connection", "Незащитена връзка"), + ("Secure Connection", "Сигурна връзка"), + ("Insecure Connection", "Несигурна връзка"), ("Scale original", "Оригинален мащаб"), - ("Scale adaptive", "Адаптивно мащабиране"), + ("Scale adaptive", "Приспособимо мащабиране"), ("General", "Основен"), ("Security", "Сигурност"), ("Theme", "Тема"), @@ -345,128 +345,129 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Светла тема"), ("Dark", "Тъмна"), ("Light", "Светла"), - ("Follow System", "Следвай системата"), - ("Enable hardware codec", "Активиране на хардуерен кодек"), + ("Follow System", "Следвай система"), + ("Enable hardware codec", "Позволяване хардуерен кодек"), ("Unlock Security Settings", "Отключи настройките за сигурност"), ("Enable audio", "Разрешете аудиото"), ("Unlock Network Settings", "Отключи мрежовите настройки"), ("Server", "Сървър"), - ("Direct IP Access", "Директен IP достъп"), - ("Proxy", "Прокси"), - ("Apply", "Приложи"), - ("Disconnect all devices?", ""), - ("Clear", "Изчисти"), + ("Direct IP Access", "Пряк IP достъп"), + ("Proxy", "Посредник (Proxy)"), + ("Apply", "Прилагане"), + ("Disconnect all devices?", "Разкачване на всички устройства"), + ("Clear", "Изчистване"), ("Audio Input Device", "Аудио входно устройство"), - ("Use IP Whitelisting", "Използвайте бял списък с IP адреси"), + ("Use IP Whitelisting", "Използване бял списък с IP адреси"), ("Network", "Мрежа"), - ("Pin Toolbar", "Фиксиране на лентата с инструменти"), - ("Unpin Toolbar", "Откачване на лентата с инструменти"), + ("Pin Toolbar", "Закачане лента с инструменти"), + ("Unpin Toolbar", "Откачюане лента с инструменти"), ("Recording", "Записване"), ("Directory", "Директория"), - ("Automatically record incoming sessions", ""), - ("Change", "Промени"), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable recording session", ""), - ("Enable LAN discovery", "Активирайте откриване в LAN"), - ("Deny LAN discovery", "Забранете откриване в LAN"), + ("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"), + ("Automatically record outgoing sessions", ""), + ("Change", "Промяна"), + ("Start session recording", "Започванена запис"), + ("Stop session recording", "Край на запис"), + ("Enable recording session", "Позволяване запис"), + ("Enable LAN discovery", "Позволяване откриване във вътрешна мрежа"), + ("Deny LAN discovery", "Забрана за откриване във вътрешна мрежа"), ("Write a message", "Напишете съобщение"), ("Prompt", "Подкана"), - ("Please wait for confirmation of UAC...", ""), + ("Please wait for confirmation of UAC...", "Моля изчакайте за потвърждение от UAC..."), ("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."), ("Disconnected", "Прекъсната връзка"), ("Other", "Други"), - ("Confirm before closing multiple tabs", ""), + ("Confirm before closing multiple tabs", "Потвърждение преди затваряне на няколко раздела"), ("Keyboard Settings", "Настройки на клавиатурата"), ("Full Access", "Пълен достъп"), ("Screen Share", "Споделяне на екрана"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", "Преглед"), - ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (Работете от страна на партньора)."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), + ("JumpLink", "Препратка"), + ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."), ("Show RustDesk", "Покажи RustDesk"), ("This PC", "Този компютър"), ("or", "или"), ("Continue with", "Продължи с"), ("Elevate", "Повишаване"), - ("Zoom cursor", "Мащабиране на Курсор"), - ("Accept sessions via password", "Приемайте сесии чрез парола"), - ("Accept sessions via click", "Приемане на сесии чрез щракване"), - ("Accept sessions via both", "Приемайте сесии и през двете"), - ("Please wait for the remote side to accept your session request...", ""), + ("Zoom cursor", "Уголемяване курсор"), + ("Accept sessions via password", "Приемане сесии чрез парола"), + ("Accept sessions via click", "Приемане сесии чрез цъкване"), + ("Accept sessions via both", "Приемане сесии и по двата начина"), + ("Please wait for the remote side to accept your session request...", "Моля, изчакайте докато другата страна приеме заявката за отдалечен достъп..."), ("One-time Password", "Еднократна парола"), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", "Разрешете скриването само ако приемате сесии чрез парола и използвате постоянна парола"), - ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), - ("Right click to select tabs", ""), + ("Use one-time password", "Ползване на еднократна парола"), + ("One-time password length", "Дължина на еднократна парола"), + ("Request access to your device", "Искане за достъп до ваше устройство"), + ("Hide connection management window", "Скриване на прозореца за управление на свързване"), + ("hide_cm_tip", "Разрешаване скриване само ако се приемат сесии чрез постоянна парола"), + ("wayland_experiment_tip", "Поддръжката на Wayland е в експериментален стадий, моля, използвайте X11, ако се нуждаете от безконтролен достъп.."), + ("Right click to select tabs", "Десен бутон за избор на раздел"), ("Skipped", "Пропуснато"), - ("Add to address book", ""), + ("Add to address book", "Добавяне към познати адреси"), ("Group", "Група"), ("Search", "Търсене"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by web console", "Затворен ръчно от уеб конзола"), + ("Local keyboard type", "Тип на тукашната клавиатура"), + ("Select local keyboard type", "Избор на тип на тукашната клавиатура"), ("software_render_tip", "Ако използвате графична карта Nvidia под Linux и отдалеченият прозорец се затваря веднага след свързване, превключването към драйвера Nouveau с отворен код и изборът да използвате софтуерно изобразяване може да помогне. Изисква се рестартиране на софтуера."), - ("Always use software rendering", ""), - ("config_input", "За да контролирате отдалечен работен плот с клавиатура, трябва да предоставите на RustDesk разрешения \"Input Monitoring\"."), - ("config_microphone", "За да говорите дистанционно, трябва да предоставите на RustDesk разрешения \"Запис на звук\"."), - ("request_elevation_tip", "Можете също така да поискате повишаване на привилегии, ако има някой от отдалечената страна."), - ("Wait", "Изчакайте"), - ("Elevation Error", "Грешка при повишаване на привилегии"), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиранят RustDesk."), - ("Request Elevation", "Поискайте повишаване на привилегии"), + ("Always use software rendering", "Винаги ползвай софтуерно изграждане на картината"), + ("config_input", "За да управлявате отдалечена среда с клавиатура, трябва да предоставите на RustDesk право за \"Input Monitoring\"."), + ("config_microphone", "За да говорите отдалечено, трябва да предоставите на RustDesk право за \"Запис на звук\"."), + ("request_elevation_tip", "Можете също така да поискате разширени права, ако има някой от отдалечената страна."), + ("Wait", "Изчакване"), + ("Elevation Error", "Грешка при добвиане на разширени права"), + ("Ask the remote user for authentication", "Попитайте отдалечения потребител за удостоверяване"), + ("Choose this if the remote account is administrator", "Изберете това, ако отдалеченият потребител е администратор."), + ("Transmit the username and password of administrator", "Предаване на потребителското име и паролата на администратора"), + ("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиран RustDesk."), + ("Request Elevation", "Поискайте разширени права"), ("wait_accept_uac_tip", "Моля, изчакайте отдалеченият потребител да приеме диалоговия прозорец на UAC."), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", "Сменете страните"), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Elevate successfully", "Успешно получаване на разширени права"), + ("uppercase", "големи букви"), + ("lowercase", "малки букви"), + ("digit", "цифра"), + ("special character", "специален знак"), + ("length>=8", "дължина>=8"), + ("Weak", "Слаба"), + ("Medium", "Средна"), + ("Strong", "Силна"), + ("Switch Sides", "Размяна на страните"), + ("Please confirm if you want to share your desktop?", "Моля, потвърдете дали искате да споделите работното си пространство"), + ("Display", "Екран"), ("Default View Style", "Стил на изглед по подразбиране"), ("Default Scroll Style", "Стил на превъртане по подразбиране"), ("Default Image Quality", "Качество на изображението по подразбиране"), ("Default Codec", "Кодек по подразбиране"), - ("Bitrate", "Битрейт"), + ("Bitrate", "Скорост на предаване на данни (bitrate)"), ("FPS", "Кадри в секунда"), ("Auto", "Автоматично"), ("Other Default Options", "Други опции по подразбиране"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Гласови обаждания"), + ("Text chat", "Текстов разговор"), + ("Stop voice call", "Прекратяване гласово обаждане"), ("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез реле. Освен това, ако искате да използвате реле при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез реле\" в картата на последните сесии, ако съществува."), - ("Reconnect", "Свържете се отново"), + ("Reconnect", "Повторно свързане"), ("Codec", "Кодек"), - ("Resolution", "Резолюция"), - ("No transfers in progress", "Не се извършват трансфери"), - ("Set one-time password length", ""), + ("Resolution", "Разделителна способност"), + ("No transfers in progress", "Няма текущи прехвърляния"), + ("Set one-time password length", "Задаване дължаина на еднократна парола"), ("RDP Settings", "RDP настройки"), - ("Sort by", "Сортирай по"), + ("Sort by", "Подредба по"), ("New Connection", "Ново свързване"), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Restore", "Възстановяване"), + ("Minimize", "Смаляване"), + ("Maximize", "Уголемяване"), ("Your Device", "Вашето устройство"), ("empty_recent_tip", "Ами сега, няма скорошни сесии!\nВреме е да планирате нова."), ("empty_favorite_tip", "Все още нямате любими връстници?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"), ("empty_lan_tip", "О, не, изглежда, че все още не сме открили връстници."), ("empty_address_book_tip", "Изглежда, че в момента няма изброени връстници във вашата адресна книга."), - ("eg: admin", ""), + ("eg: admin", "напр. admin"), ("Empty Username", "Празно потребителско име"), ("Empty Password", "Празна парола"), - ("Me", "Аз"), - ("identical_file_tip", "Този файл е идентичен с този на партньора."), + ("Me", "Мен"), + ("identical_file_tip", "Файлът съвпада с този от другата страна."), ("show_monitors_tip", "Показване на мониторите в лентата с инструменти"), ("View Mode", "Режим на преглед"), ("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"), @@ -482,47 +483,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"), ("No need to elevate", ""), ("System Sound", "Системен звук"), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), + ("Default", "По подразбиране"), + ("New RDP", "Нов RDP"), + ("Fingerprint", "Пръстов отпечатък"), ("Copy Fingerprint", "Копиране на пръстов отпечатък"), ("no fingerprints", "Няма пръстови отпечатъци"), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), + ("Select a peer", "Избери отдалечена страна"), + ("Select peers", "Избери отдалечени страни"), + ("Plugins", "Приставки"), + ("Uninstall", "Премахни"), + ("Update", "Обновяване"), + ("Enable", "Позволяване"), + ("Disable", "Забрана"), ("Options", "Настроики"), - ("resolution_original_tip", "Оригинална резолюция"), - ("resolution_fit_local_tip", "Напасване към локална разделителна способност"), - ("resolution_custom_tip", "Персонализирана разделителна способност"), + ("resolution_original_tip", "Оригинална разделителна способност"), + ("resolution_fit_local_tip", "Приспособяване към тукашната разделителна способност"), + ("resolution_custom_tip", "Разделителна способност по свой избор"), ("Collapse toolbar", "Свиване на лентата с инструменти"), - ("Accept and Elevate", "Приемете и повишаване на привилегии"), - ("accept_and_elevate_btn_tooltip", "Приемете връзката и повишете UAC разрешенията."), - ("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за копиране изтече."), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", "Излез"), - ("Open", "Отвори"), + ("Accept and Elevate", "Приемане и предоставяне на допълнителни права"), + ("accept_and_elevate_btn_tooltip", "Приемане на връзката предоставяне на UAC разрешения."), + ("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за препис изтече."), + ("Incoming connection", "Входяща връзка"), + ("Outgoing connection", "Изходяща връзка"), + ("Exit", "Изход"), + ("Open", "Отваряне"), ("logout_tip", "Сигурни ли сте, че искате да излезете?"), ("Service", "Услуга"), ("Start", "Стартиране"), ("Stop", "Спиране"), ("exceed_max_devices", "Достигнахте максималния брой управлявани устройства."), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", "Промяна на паролата"), - ("Refresh Password", "Обнови паролата"), - ("ID", ""), + ("Sync with recent sessions", "Синхронизиране с последните сесии"), + ("Sort tags", "Подреди белези"), + ("Open connection in new tab", "Разкриване на връзка в нов раздел"), + ("Move tab to new window", "Отделяне на раздела в нов прозорец"), + ("Can not be empty", "Не може да е празно"), + ("Already exists", "Вече съществува"), + ("Change Password", "Промяна на парола"), + ("Refresh Password", "Обновяване парола"), + ("ID", "Определител (ID)"), ("Grid View", "Мрежов изглед"), ("List View", "Списъчен изглед"), - ("Select", ""), + ("Select", "Избиране"), ("Toggle Tags", "Превключване на етикети"), ("pull_ab_failed_tip", "Неуспешно опресняване на адресната книга"), ("push_ab_failed_tip", "Неуспешно синхронизиране на адресната книга със сървъра"), @@ -530,119 +531,127 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change Color", "Промяна на цвета"), ("Primary Color", "Основен цвят"), ("HSV Color", "HSV цвят"), - ("Installation Successful!", "Успешна инсталация!"), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), + ("Installation Successful!", "Успешно поставяне!"), + ("Installation failed!", "Провал при поставяне"), + ("Reverse mouse wheel", "Обърнато колелото на мишката"), + ("{} sessions", "{} сесии"), ("scam_title", "Възможно е да сте ИЗМАМЕНИ!"), ("scam_text1", "Ако разговаряте по телефона с някой, когото НЕ ПОЗНАВАТЕ и НЯМАТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да стартирате услугата, не продължавайте и затворете незабавно."), ("scam_text2", "Те вероятно са измамник, който се опитва да открадне вашите пари или друга лична информация."), ("Don't show again", "Не показвай отново"), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), + ("I Agree", "Съгласен"), + ("Decline", "Отказвам"), + ("Timeout in minutes", "Време за отговор в минути"), ("auto_disconnect_option_tip", "Автоматично затваряне на входящите сесии при неактивност на потребителя"), ("Connection failed due to inactivity", "Автоматично прекъсване на връзката поради неактивност"), ("Check for software update on startup", ""), ("upgrade_rustdesk_server_pro_to_{}_tip", "Моля обновете RustDesk Server Pro на версия {} или по-нова!"), ("pull_group_failed_tip", "Неуспешно опресняване на групата"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Filter by intersection", "Отсяване по пресичане"), + ("Remove wallpaper during incoming sessions", "Спри фоновото изображение по време на входящи сесии"), + ("Test", "Проверка"), ("display_is_plugged_out_msg", "Дисплеят е изключен, превключете на първия монитор."), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), + ("No displays", "Няма екрани"), + ("Open in new window", "Отваряне в нов прозорец"), + ("Show displays as individual windows", "Показване на екраните в отделни прозорци"), + ("Use all my displays for the remote session", "Използване на всички тукашни екрани за отдалечена работа"), ("selinux_tip", "SELinux е активиран на вашето устройство, което може да попречи на RustDesk да работи правилно като контролирана страна."), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), + ("Change view", "Промяна изглед"), + ("Big tiles", "Големи заглавия"), + ("Small tiles", "Малки заглавия"), + ("List", "Списък"), + ("Virtual display", "Виртуален екран"), + ("Plug out all", "Изтръгване на всички"), ("True color (4:4:4)", ""), ("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"), ("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (:).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (@?key=), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"@public\" , ключът не е необходим за публичен сървър"), ("privacy_mode_impl_mag_tip", "Режим 1"), ("privacy_mode_impl_virtual_display_tip", "Режим 2"), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), + ("Enter privacy mode", "Влизане в поверителен режим"), + ("Exit privacy mode", "Изход от поверителен режим"), ("idd_not_support_under_win10_2004_tip", "Индиректен драйвер за дисплей не се поддържа. Изисква се Windows 10, версия 2004 или по-нова."), ("input_source_1_tip", "Входен източник 1"), ("input_source_2_tip", "Входен източник 2"), ("Swap control-command key", ""), - ("swap-left-right-mouse", "Разменете левия и десния бутон на мишката"), - ("2FA code", "Код за Двуфакторна удостоверяване"), + ("swap-left-right-mouse", "Размяна на копчетата на мишката"), + ("2FA code", "Код за Двуфакторно удостоверяване"), ("More", "Повече"), - ("enable-2fa-title", "Активиране на двуфакторно удостоверяване"), + ("enable-2fa-title", "Позволяване на двуфакторно удостоверяване"), ("enable-2fa-desc", "Моля, настройте вашия удостоверител сега. Можете да използвате приложение за удостоверяване като Authy, Microsoft или Google Authenticator на вашия телефон или настолен компютър.\n\nСканирайте QR кода с вашето приложение и въведете кода, който приложението ви показва, за да активирате двуфакторно удостоверяване."), ("wrong-2fa-code", "е може да се потвърди кодът. Проверете дали настройките за код и локалното време са правилни"), ("enter-2fa-title", "Двуфакторно удостоверяване"), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), + ("Email verification code must be 6 characters.", "Кодът за проверка следва да е с дължина 6 знака."), + ("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"), + ("Multiple Windows sessions found", "Установени са няколко Windwos сесии"), + ("Please select the session you want to connect to", "Моля определете сесия към която искате да се свърженете"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), + ("Security Alert", "Предупреждение за сигурност"), + ("My address book", "Моята адресна книга"), + ("Personal", "Личен"), + ("Owner", "Собственик"), + ("Set shared password", "Определяне споделена парола"), + ("Exist in", "Съществува в"), + ("Read-only", "Само четене"), + ("Read/Write", "Писане/четене"), + ("Full Control", "Пълен контрол"), ("share_warning_tip", ""), - ("Everyone", ""), + ("Everyone", "Всички"), ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Следвай отдалечения курсор"), + ("Follow remote window focus", "Следвай фокуса на отдалечените прозорци"), ("default_proxy_tip", ""), ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), + ("Incoming", "Входящ"), + ("Outgoing", "Изходящ"), + ("Clear Wayland screen selection", "Изчистване избор на Wayland екран"), ("clear_Wayland_screen_selection_tip", ""), ("confirm_clear_Wayland_screen_selection_tip", ""), ("android_new_voice_call_tip", ""), ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), + ("Use texture rendering", "Използвай текстово изграждане"), + ("Floating window", "Плаващ прозорец"), ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), + ("Keep screen on", "Запази екранът включен"), + ("Never", "Никога"), + ("During controlled", "Докато е обект на управление"), + ("During service is on", "Докато услугата е включена"), + ("Capture screen using DirectX", "Снемай екрана ползвайки DirectX"), ("Back", "Назад"), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), + ("Apps", "Приложения"), + ("Volume up", "Усилване звук"), + ("Volume down", "Намаляне звук"), + ("Power", "Мощност"), + ("Telegram bot", "Телеграм бот"), ("enable-bot-tip", ""), ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("About RustDesk", "Относно RustDesk"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), - ("Unlock with PIN", ""), + ("Unlock with PIN", "Отключване с PIN"), ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), + ("Wrong PIN", "Грешен PIN"), + ("Set PIN", "Избор PIN"), + ("Enable trusted devices", "Позволяване доверени устройства"), + ("Manage trusted devices", "Управление доверени устройства"), + ("Platform", "Платформа"), + ("Days remaining", "Оставащи дни"), ("enable-trusted-devices-tip", ""), ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Resume", "Възобновяване"), + ("Invalid file name", "Невалидно име за файл"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0aa64ec28e6..8e0ff1479cc 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -2,647 +2,656 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Estat"), - ("Your Desktop", "El teu escriptori"), - ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), + ("Your Desktop", "Aquest ordinador"), + ("desk_tip", "Es pot accedir a aquest equip mitjançant les credencials:"), ("Password", "Contrasenya"), - ("Ready", "Llest"), - ("Established", "Establert"), - ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), - ("Enable service", "Habilita el servei"), - ("Start service", "Inicia el servei"), - ("Service is running", "El servei s'està executant"), - ("Service is not running", "El servei no s'està executant"), - ("not_ready_status", "No està llest. Comprova la teva connexió"), - ("Control Remote Desktop", "Controlar escriptori remot"), - ("Transfer file", "Transferir arxiu"), - ("Connect", "Connectar"), + ("Ready", "Preparat."), + ("Established", "S'ha establert."), + ("connecting_status", "S'està connectant a la xarxa de RustDesk..."), + ("Enable service", "Habilita el servei."), + ("Start service", "Inicia el servei."), + ("Service is running", "El servei s'està executant."), + ("Service is not running", "El servei no s'està executant."), + ("not_ready_status", "No disponible. Verifiqueu la connexió"), + ("Control Remote Desktop", "Dispositiu remot"), + ("Transfer file", "Transfereix fitxers"), + ("Connect", "Connecta"), ("Recent sessions", "Sessions recents"), - ("Address book", "Directori"), + ("Address book", "Llibreta d'adreces"), ("Confirmation", "Confirmació"), ("TCP tunneling", "Túnel TCP"), - ("Remove", "Eliminar"), + ("Remove", "Suprimeix"), ("Refresh random password", "Actualitza la contrasenya aleatòria"), - ("Set your own password", "Estableix la teva contrasenya"), + ("Set your own password", "Establiu la vostra contrasenya"), ("Enable keyboard/mouse", "Habilita el teclat/ratolí"), - ("Enable clipboard", "Habilita el portapapers"), - ("Enable file transfer", "Habilita la transferència d'arxius"), + ("Enable clipboard", "Habilita el porta-retalls"), + ("Enable file transfer", "Habilita la transferència de fitxers"), ("Enable TCP tunneling", "Habilita el túnel TCP"), ("IP Whitelisting", "Adreces IP admeses"), - ("ID/Relay Server", "Servidor ID/Relay"), + ("ID/Relay Server", "ID/Repetidor del Servidor"), ("Import server config", "Importa la configuració del servidor"), ("Export Server Config", "Exporta la configuració del servidor"), - ("Import server configuration successfully", "Configuració del servidor importada amb èxit"), - ("Export server configuration successfully", "Configuració del servidor exportada con èxit"), - ("Invalid server configuration", "Configuració del servidor incorrecta"), - ("Clipboard is empty", "El portapapers està buit"), + ("Import server configuration successfully", "S'ha importat la configuració del servidor correctament"), + ("Export server configuration successfully", "S'ha exportat la configuració del servidor correctament"), + ("Invalid server configuration", "Configuració del servidor no vàlida"), + ("Clipboard is empty", "El porta-retalls és buit"), ("Stop service", "Atura el servei"), - ("Change ID", "Canviar ID"), - ("Your new ID", "La teva nova ID"), - ("length %min% to %max%", "Ha de tenir entre %min% i %max% caràcters"), - ("starts with a letter", "comença amb una lletra"), - ("allowed characters", "caràcters permesos"), - ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), + ("Change ID", "Canvia la ID"), + ("Your new ID", "Identificador nou"), + ("length %min% to %max%", "Entre %min% i %max% caràcters"), + ("starts with a letter", "Comença amb una lletra"), + ("allowed characters", "Caràcters admesos"), + ("id_change_tip", "Els caràcters admesos són: a-z, A-Z, 0-9, _ (guió baix). El primer caràcter ha de ser a-z/A-Z, i una mida de 6 a 16 caràcters."), ("Website", "Lloc web"), - ("About", "Sobre"), - ("Slogan_tip", ""), - ("Privacy Statement", "Declaració de privacitat"), + ("About", "Quant al RustDesk"), + ("Slogan_tip", "Fet de tot cor dins d'aquest món caòtic!\nTraducció: Benet R. i Camps (BennyBeat)."), + ("Privacy Statement", "Declaració de privadesa"), ("Mute", "Silencia"), - ("Build Date", "Data de creació"), + ("Build Date", "Data de compilació"), ("Version", "Versió"), ("Home", "Inici"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), - ("Hardware Codec", "Còdec de hardware"), - ("Adaptive bitrate", "Tasa de bits adaptativa"), - ("ID Server", "Servidor de IDs"), - ("Relay Server", "Servidor Relay"), - ("API Server", "Servidor API"), + ("Hardware Codec", "Codificació per maquinari"), + ("Adaptive bitrate", "Taxa de bits adaptativa"), + ("ID Server", "ID del servidor"), + ("Relay Server", "Repetidor del servidor"), + ("API Server", "Clau API del servidor"), ("invalid_http", "ha de començar amb http:// o https://"), - ("Invalid IP", "IP incorrecta"), - ("Invalid format", "Format incorrecte"), - ("server_not_support", "Encara no és compatible amb el servidor"), + ("Invalid IP", "IP no vàlida"), + ("Invalid format", "Format no vàlid"), + ("server_not_support", "Encara no suportat pel servidor"), ("Not available", "No disponible"), - ("Too frequent", "Massa comú"), + ("Too frequent", "Massa freqüent"), ("Cancel", "Cancel·la"), - ("Skip", "Salta"), - ("Close", "Tanca"), - ("Retry", "Reintenta"), + ("Skip", "Omet"), + ("Close", "Surt"), + ("Retry", "Torna a provar"), ("OK", "D'acord"), - ("Password Required", "Es necessita la contrasenya"), - ("Please enter your password", "Introdueix la teva contrasenya"), + ("Password Required", "Contrasenya requerida"), + ("Please enter your password", "Inseriu la contrasenya"), ("Remember password", "Recorda la contrasenya"), - ("Wrong Password", "Contrasenya incorrecta"), - ("Do you want to enter again?", "Vols tornar a entrar?"), + ("Wrong Password", "Contrasenya no vàlida"), + ("Do you want to enter again?", "Voleu tornar a provar?"), ("Connection Error", "Error de connexió"), ("Error", "Error"), - ("Reset by the peer", "Reestablert pel peer"), - ("Connecting...", "Connectant..."), - ("Connection in progress. Please wait.", "Connexió en procés. Espera."), - ("Please try 1 minute later", "Torna a provar-ho d'aquí un minut"), - ("Login Error", "Error d'inici de sessió"), - ("Successful", "Exitós"), - ("Connected, waiting for image...", "Connectant, esperant imatge..."), + ("Reset by the peer", "Restablert pel client"), + ("Connecting...", "S'està connectant..."), + ("Connection in progress. Please wait.", "S'està connectant. Espereu..."), + ("Please try 1 minute later", "Torneu a provar en 1 minut"), + ("Login Error", "Error d'accés"), + ("Successful", "Correcte"), + ("Connected, waiting for image...", "S'ha connectat; en espera de rebre la imatge..."), ("Name", "Nom"), ("Type", "Tipus"), ("Modified", "Modificat"), - ("Size", "Grandària"), - ("Show Hidden Files", "Mostra arxius ocults"), + ("Size", "Mida"), + ("Show Hidden Files", "Mostra els fitxers ocults"), ("Receive", "Rep"), ("Send", "Envia"), - ("Refresh File", "Actualitza el fitxer"), + ("Refresh File", "Actualitza"), ("Local", "Local"), ("Remote", "Remot"), - ("Remote Computer", "Ordinador remot"), - ("Local Computer", "Ordinador local"), - ("Confirm Delete", "Confirma l'eliminació"), - ("Delete", "Elimina"), + ("Remote Computer", "Dispositiu remot"), + ("Local Computer", "Aquest ordinador"), + ("Confirm Delete", "Confirmació de supressió"), + ("Delete", "Suprimeix"), ("Properties", "Propietats"), ("Multi Select", "Selecció múltiple"), - ("Select All", "Selecciona-ho tot"), - ("Unselect All", "Deselecciona-ho tot"), + ("Select All", "Seleciona-ho tot"), + ("Unselect All", "Desselecciona-ho tot"), ("Empty Directory", "Carpeta buida"), ("Not an empty directory", "No és una carpeta buida"), - ("Are you sure you want to delete this file?", "Segur que vols eliminar aquest fitxer?"), - ("Are you sure you want to delete this empty directory?", "Segur que vols eliminar aquesta carpeta buida?"), - ("Are you sure you want to delete the file of this directory?", "Segur que vols eliminar aquest fitxer d'aquesta car`peta?"), - ("Do this for all conflicts", "Fes això per a tots els conflictes"), - ("This is irreversible!", "Això és irreversible!"), - ("Deleting", "Eliminant"), + ("Are you sure you want to delete this file?", "Segur que voleu suprimir aquest fitxer?"), + ("Are you sure you want to delete this empty directory?", "Segur que voleu suprimir aquesta carpeta buida?"), + ("Are you sure you want to delete the file of this directory?", "Segur que voleu suprimir el fitxer d'aquesta carpeta?"), + ("Do this for all conflicts", "Aplica aquesta acció per a tots els conflictes"), + ("This is irreversible!", "Aquesta acció no es pot desfer!"), + ("Deleting", "S'està suprimint"), ("files", "fitxers"), - ("Waiting", "Esperant"), - ("Finished", "Acabat"), + ("Waiting", "En espera"), + ("Finished", "Ha finalitzat"), ("Speed", "Velocitat"), ("Custom Image Quality", "Qualitat d'imatge personalitzada"), ("Privacy mode", "Mode privat"), - ("Block user input", "Bloqueja l'entrada d'usuari"), - ("Unblock user input", "Desbloqueja l'entrada d'usuari"), + ("Block user input", "Bloca el control a l'usuari"), + ("Unblock user input", "Desbloca el control a l'usuari"), ("Adjust Window", "Ajusta la finestra"), ("Original", "Original"), - ("Shrink", "Reduir"), - ("Stretch", "Estirar"), + ("Shrink", "Encongida"), + ("Stretch", "Ampliada"), ("Scrollbar", "Barra de desplaçament"), ("ScrollAuto", "Desplaçament automàtic"), ("Good image quality", "Bona qualitat d'imatge"), - ("Balanced", "Equilibrat"), + ("Balanced", "Equilibrada"), ("Optimize reaction time", "Optimitza el temps de reacció"), - ("Custom", "Personalitzat"), + ("Custom", "Personalitzada"), ("Show remote cursor", "Mostra el cursor remot"), - ("Show quality monitor", "Mostra la qualitat del monitor"), - ("Disable clipboard", "Deshabilita el portapapers"), - ("Lock after session end", "Bloqueja després del final de la sessió"), - ("Insert", "Insereix"), - ("Insert Lock", "Insereix bloqueig"), + ("Show quality monitor", "Mostra la informació de flux"), + ("Disable clipboard", "Inhabilita el porta-retalls"), + ("Lock after session end", "Bloca en finalitzar la sessió"), + ("Insert Ctrl + Alt + Del", "Insereix Ctrl + Alt + Del"), + ("Insert Lock", "Bloca"), ("Refresh", "Actualitza"), - ("ID does not exist", "L'ID no existeix"), - ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), - ("Please try later", "Prova-ho més tard"), - ("Remote desktop is offline", "L'escriptori remot està desconecctat"), + ("ID does not exist", "Aquesta ID no existeix"), + ("Failed to connect to rendezvous server", "Ha fallat en connectar al servidor assignat"), + ("Please try later", "Proveu més tard"), + ("Remote desktop is offline", "El dispositiu remot està desconnectat"), ("Key mismatch", "La clau no coincideix"), - ("Timeout", "Temps esgotat"), - ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), - ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), - ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), - ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), - ("Set Password", "Configura la contrasenya"), - ("OS Password", "contrasenya del sistema operatiu"), - ("install_tip", ""), - ("Click to upgrade", "Clica per a actualitzar"), - ("Click to download", "Clica per a descarregar"), - ("Click to update", "Clica per a refrescar"), - ("Configure", "Configurr"), - ("config_acc", ""), - ("config_screen", "Configurar pantalla"), - ("Installing ...", "Instal·lant ..."), + ("Timeout", "S'ha exhaurit el temps"), + ("Failed to connect to relay server", "Ha fallat en connectar amb el repetidor del servidor"), + ("Failed to connect via rendezvous server", "Ha fallat en connectar mitjançant el servidor assignat"), + ("Failed to connect via relay server", "Ha fallat en connectar mitjançant el repetidor del servidor"), + ("Failed to make direct connection to remote desktop", "Ha fallat la connexió directa amb el dispositiu remot"), + ("Set Password", "Establiu una contrasenya"), + ("OS Password", "Contrasenya del sistema"), + ("install_tip", "En alguns casos és possible que el RustDesk no funcioni correctament per les restriccions UAC («User Account Control»; Control de comptes d'usuari). Per evitar aquest problema, instal·leu el RustDesk al vostre sistema."), + ("Click to upgrade", "Feu clic per a actualitzar"), + ("Click to download", "Feu clic per a baixar"), + ("Click to update", "Feu clic per a actualitzar"), + ("Configure", "Configura"), + ("config_acc", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos d'accessibilitat."), + ("config_screen", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos de gravació de pantalla."), + ("Installing ...", "S'està instal·lant..."), ("Install", "Instal·la"), ("Installation", "Instal·lació"), - ("Installation Path", "Ruta d'instal·lació"), - ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), - ("Create desktop icon", "Crear icona d'escriptori"), - ("agreement_tip", ""), - ("Accept and Install", "Acceptar i instal·la"), + ("Installation Path", "Ruta de la instal·lació"), + ("Create start menu shortcuts", "Crea una drecera al menú d'inici"), + ("Create desktop icon", "Crea una icona a l'escriptori"), + ("agreement_tip", "En iniciar la instal·lació, esteu acceptant l'acord de llicència d'usuari."), + ("Accept and Install", "Accepta i instal·la"), ("End-user license agreement", "Acord de llicència d'usuari final"), - ("Generating ...", "Generant ..."), - ("Your installation is lower version.", "La teva instal·lació és una versión inferior."), - ("not_close_tcp_tip", ""), - ("Listening ...", "Escoltant..."), - ("Remote Host", "Hoste remot"), + ("Generating ...", "S'està generant..."), + ("Your installation is lower version.", "La instal·lació actual és una versió inferior"), + ("not_close_tcp_tip", "No tanqueu aquesta finestra mentre utilitzeu el túnel"), + ("Listening ...", "S'està escoltant..."), + ("Remote Host", "Amfitrió remot"), ("Remote Port", "Port remot"), ("Action", "Acció"), ("Add", "Afegeix"), ("Local Port", "Port local"), - ("Local Address", "Adreça Local"), + ("Local Address", "Adreça local"), ("Change Local Port", "Canvia el port local"), - ("setup_server_tip", ""), - ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), - ("The confirmation is not identical.", "La confirmació no coincideix."), + ("setup_server_tip", "Per a connexions més ràpides o privades, configureu el vostre servidor"), + ("Too short, at least 6 characters.", "Massa curt. Són necessaris almenys 6 caràcters."), + ("The confirmation is not identical.", "Les contrasenyes no coincideixen."), ("Permissions", "Permisos"), ("Accept", "Accepta"), - ("Dismiss", "Cancel·la"), + ("Dismiss", "Ignora"), ("Disconnect", "Desconnecta"), - ("Enable file copy and paste", "Permet copiar i enganxar fitxers"), + ("Enable file copy and paste", "Habilita la còpia i enganxament de fitxers"), ("Connected", "Connectat"), - ("Direct and encrypted connection", "Connexió directa i xifrada"), - ("Relayed and encrypted connection", "Connexió retransmesa i xifrada"), - ("Direct and unencrypted connection", "Connexió directa i sense xifrar"), - ("Relayed and unencrypted connection", "Connexió retransmesa i sense xifrar"), - ("Enter Remote ID", "Introdueix l'ID remot"), - ("Enter your password", "Introdueix la teva contrasenya"), - ("Logging in...", "Iniciant sessió..."), + ("Direct and encrypted connection", "Connexió xifrada directa"), + ("Relayed and encrypted connection", "Connexió xifrada per repetidor"), + ("Direct and unencrypted connection", "Connexió directa sense xifratge"), + ("Relayed and unencrypted connection", "Connexió per repetidor sense xifratge"), + ("Enter Remote ID", "Inseriu la ID remota"), + ("Enter your password", "Inseriu la contrasenya"), + ("Logging in...", "S'està iniciant..."), ("Enable RDP session sharing", "Habilita l'ús compartit de sessions RDP"), ("Auto Login", "Inici de sessió automàtic"), - ("Enable direct IP access", "Habilita accés IP directe"), - ("Rename", "Renombra"), + ("Enable direct IP access", "Habilita l'accés directe per IP"), + ("Rename", "Reanomena"), ("Space", "Espai"), - ("Create desktop shortcut", "Crea accés directe a l'escriptori"), + ("Create desktop shortcut", "Crea una drecera a l'escriptori"), ("Change Path", "Canvia la ruta"), - ("Create Folder", "Crea carpeta"), - ("Please enter the folder name", "Indica el nom de la carpeta"), - ("Fix it", "Soluciona-ho"), - ("Warning", "Avís"), - ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), + ("Create Folder", "Carpeta nova"), + ("Please enter the folder name", "Inseriu el nom de la carpeta"), + ("Fix it", "Repara"), + ("Warning", "Atenció"), + ("Login screen using Wayland is not supported", "L'inici de sessió amb Wayland encara no és compatible"), ("Reboot required", "Cal reiniciar"), ("Unsupported display server", "Servidor de visualització no compatible"), ("x11 expected", "x11 necessari"), ("Port", "Port"), - ("Settings", "Ajustaments"), - ("Username", " Nom d'usuari"), - ("Invalid port", "Port incorrecte"), - ("Closed manually by the peer", "Tancat manualment pel peer"), - ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), - ("Run without install", "Executa sense instal·lar"), - ("Connect via relay", "Connecta per relay"), - ("Always connect via relay", "Connecta sempre a través de relay"), - ("whitelist_tip", ""), - ("Login", "Inicia sessió"), + ("Settings", "Configuració"), + ("Username", "Nom d'usuari"), + ("Invalid port", "Port no vàlid"), + ("Closed manually by the peer", "Tancat manualment pel client"), + ("Enable remote configuration modification", "Habilita la modificació remota de la configuració"), + ("Run without install", "Inicia sense instal·lar"), + ("Connect via relay", "Connecta mitjançant un repetidor"), + ("Always connect via relay", "Connecta sempre mitjançant un repetidor"), + ("whitelist_tip", "Només les IP admeses es podran connectar"), + ("Login", "Inicia la sessió"), ("Verify", "Verifica"), ("Remember me", "Recorda'm"), ("Trust this device", "Confia en aquest dispositiu"), ("Verification code", "Codi de verificació"), - ("verification_tip", ""), - ("Logout", "Sortir"), + ("verification_tip", "S'ha enviat un codi de verificació al correu-e registrat. Inseriu-lo per a continuar amb l'inici de sessió."), + ("Logout", "Tanca la sessió"), ("Tags", "Etiquetes"), - ("Search ID", "Cerca ID"), - ("whitelist_sep", ""), - ("Add ID", "Afegir ID"), - ("Add Tag", "Afegir tag"), - ("Unselect all tags", "Deseleccionar tots els tags"), - ("Network error", "Error de xarxa"), - ("Username missed", "Nom d'usuari oblidat"), - ("Password missed", "Contrasenya oblidada"), - ("Wrong credentials", "Credencials incorrectes"), - ("The verification code is incorrect or has expired", "El codi de verificació es incorrecte o ha expirat"), - ("Edit Tag", "Editar tag"), + ("Search ID", "Cerca per ID"), + ("whitelist_sep", "Separades per coma, punt i coma, espai o una adreça per línia"), + ("Add ID", "Afegeix una ID"), + ("Add Tag", "Afegeix una etiqueta"), + ("Unselect all tags", "Desselecciona totes les etiquetes"), + ("Network error", "Error de la xarxa"), + ("Username missed", "No s'ha indicat el nom d'usuari"), + ("Password missed", "No s'ha indicat la contrasenya"), + ("Wrong credentials", "Credencials errònies"), + ("The verification code is incorrect or has expired", "El codi de verificació no és vàlid o ha caducat"), + ("Edit Tag", "Edita l'etiqueta"), ("Forget Password", "Contrasenya oblidada"), ("Favorites", "Preferits"), - ("Add to Favorites", "Afegir a preferits"), - ("Remove from Favorites", "Treure de preferits"), - ("Empty", "Buit"), - ("Invalid folder name", "Nom de carpeta incorrecte"), - ("Socks5 Proxy", "Proxy Socks5"), - ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Add to Favorites", "Afegeix als preferits"), + ("Remove from Favorites", "Suprimeix dels preferits"), + ("Empty", "Buida"), + ("Invalid folder name", "Nom de carpeta no vàlid"), + ("Socks5 Proxy", "Servidor intermediari Socks5"), + ("Socks5/Http(s) Proxy", "Servidor intermediari Socks5/Http(s)"), ("Discovered", "Descobert"), - ("install_daemon_tip", ""), - ("Remote ID", "ID remot"), + ("install_daemon_tip", "Per a iniciar durant l'arrencada del sistema, heu d'instal·lar el servei."), + ("Remote ID", "ID remota"), ("Paste", "Enganxa"), - ("Paste here?", "Enganxa-ho aquí?"), - ("Are you sure to close the connection?", "Segur que vols tancar la connexió?"), - ("Download new version", "Descarrega una nova versió"), + ("Paste here?", "Voleu enganxar aquí?"), + ("Are you sure to close the connection?", "Segur que voleu finalitzar la connexió?"), + ("Download new version", "Baixa la versió nova"), ("Touch mode", "Mode tàctil"), ("Mouse mode", "Mode ratolí"), - ("One-Finger Tap", "Toca amb un dit"), - ("Left Mouse", "Ratolí esquerra"), - ("One-Long Tap", "Toc llarg"), - ("Two-Finger Tap", "Toqui amb dos dits"), + ("One-Finger Tap", "Toc amb un dit"), + ("Left Mouse", "Botó esquerre"), + ("One-Long Tap", "Toc prolongat"), + ("Two-Finger Tap", "Toc amb dos dits"), ("Right Mouse", "Botó dret"), - ("One-Finger Move", "Moviment amb un dir"), - ("Double Tap & Move", "Toca dos cops i mogui"), - ("Mouse Drag", "Arrossega amb el ratolí"), - ("Three-Finger vertically", "Tres dits verticalment"), + ("One-Finger Move", "Moviment amb un dit"), + ("Double Tap & Move", "Toc doble i moveu"), + ("Mouse Drag", "Arrossega el ratolí"), + ("Three-Finger vertically", "Tres dits en vertical"), ("Mouse Wheel", "Roda del ratolí"), ("Two-Finger Move", "Moviment amb dos dits"), ("Canvas Move", "Moviment del llenç"), - ("Pinch to Zoom", "Pessiga per fer zoom"), - ("Canvas Zoom", "Amplia el llenç"), - ("Reset canvas", "Reestableix el llenç"), - ("No permission of file transfer", "No tens permís de transferència de fitxers"), + ("Pinch to Zoom", "Pessic per escalar"), + ("Canvas Zoom", "escala del llenç"), + ("Reset canvas", "Reinici del llenç"), + ("No permission of file transfer", "Cap permís per a transferència de fitxers"), ("Note", "Nota"), ("Connection", "Connexió"), - ("Share Screen", "Comparteix la pantalla"), + ("Share Screen", "Compartició de pantalla"), ("Chat", "Xat"), ("Total", "Total"), - ("items", "ítems"), + ("items", "elements"), ("Selected", "Seleccionat"), ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control d'entrada"), ("Audio Capture", "Captura d'àudio"), - ("File Connection", "Connexió d'arxius"), + ("File Connection", "Connexió de fitxer"), ("Screen Connection", "Connexió de pantalla"), - ("Do you accept?", "Acceptes?"), - ("Open System Setting", "Configuració del sistema obert"), - ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), - ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), - ("android_input_permission_tip2", "Vés a la pàgina de [Serveis instal·lats], activa el servei [RustDesk Input]."), - ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), - ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), - ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), - ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("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?"), + ("android_input_permission_tip1", "Per a controlar de forma remota el vostre dispositiu amb gestos o un ratolí, heu de permetre al RustDesk l'ús del servei «Accessibilitat»."), + ("android_input_permission_tip2", "A l'apartat Configuració del sistema de la pàgina següent, aneu a «Serveis baixats», i activeu el «RustDesk Input»."), + ("android_new_connection_tip", "S'ha rebut una petició nova per a controlar el vostre dispositiu."), + ("android_service_will_start_tip", "Activant «Gravació de pantalla» s'iniciarà automàticament el servei que permet a altres enviar sol·licituds de connexió cap al vostre dispositiu."), + ("android_stop_service_tip", "Tancant el servei finalitzaran automàticament les connexions en ús."), + ("android_version_audio_tip", "Aquesta versió d'Android no suporta la captura d'àudio. Actualitzeu a Android 10 o superior."), + ("android_start_service_tip", "Toqueu a «Inicia el servei» o activeu el permís «Captura de pantalla» per a iniciar el servei de compartició de pantalla."), + ("android_permission_may_not_change_tip", "Els permisos per a les connexions ja establertes poden no canviar, fins que no torneu a connectar."), ("Account", "Compte"), - ("Overwrite", "Sobreescriu"), - ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix, ometre o sobreescriure l'arxiu?"), + ("Overwrite", "Reemplaça"), + ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix. Voleu ometre o reemplaçar l'original?"), ("Quit", "Surt"), ("Help", "Ajuda"), ("Failed", "Ha fallat"), - ("Succeeded", "Aconseguit"), - ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surt"), + ("Succeeded", "Fet"), + ("Someone turns on privacy mode, exit", "S'ha activat el Mode privat; surt"), ("Unsupported", "No suportat"), - ("Peer denied", "Peer denegat"), - ("Please install plugins", "Instal·la els complements"), - ("Peer exit", "El peer ha sortit"), - ("Failed to turn off", "Error en apagar"), - ("Turned off", "Apagat"), + ("Peer denied", "Client denegat"), + ("Please install plugins", "Instal·leu els complements"), + ("Peer exit", "Finalitzat pel client"), + ("Failed to turn off", "Ha fallat en desactivar"), + ("Turned off", "Desactivat"), ("Language", "Idioma"), - ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), - ("Ignore Battery Optimizations", "Ignora optimizacions de la bateria"), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", "Engega en l'arrencada"), - ("Start the screen sharing service on boot, requires special permissions", "Engega el servei de captura de pantalla en l'arrencada, requereix permisos especials"), - ("Connection not allowed", "Connexió no disponible"), + ("Keep RustDesk background service", "Manté el servei del RustDesk en rerefons"), + ("Ignore Battery Optimizations", "Ignora les optimitzacions de bateria"), + ("android_open_battery_optimizations_tip", "Si voleu desactivar aquesta característica, feu-ho des de la pàgina següent de configuració del RustDesk, utilitzant l'opció relativa a «Bateria»"), + ("Start on boot", "Inicia durant l'arrencada"), + ("Start the screen sharing service on boot, requires special permissions", "Per iniciar la compartició de pantalla durant l'arrencada del sistema, calen permisos especials"), + ("Connection not allowed", "Connexió no permesa"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), - ("Translate mode", "Mode traduit"), - ("Use permanent password", "Utilitza una contrasenya permament"), - ("Use both passwords", "Utilitza ambdues contrasenyas"), - ("Set permanent password", "Estableix una contrasenya permament"), - ("Enable remote restart", "Activa el reinici remot"), - ("Restart remote device", "Reinicia el dispositiu"), - ("Are you sure you want to restart", "Segur que vol reiniciar?"), - ("Restarting remote device", "Reiniciant el dispositiu remot"), - ("remote_restarting_tip", "Reiniciant el dispositiu remot, tanca aquest missatge i torna't a connectar amb la contrasenya."), - ("Copied", "Copiat"), + ("Translate mode", "Mode traduït"), + ("Use permanent password", "Utilitza la contrasenya permanent"), + ("Use both passwords", "Utilitza totes dues opcions"), + ("Set permanent password", "Estableix la contrasenya permanent"), + ("Enable remote restart", "Habilita el reinici remot"), + ("Restart remote device", "Reinicia el dispositiu remot"), + ("Are you sure you want to restart", "Segur que voleu reiniciar"), + ("Restarting remote device", "Reinici del dispositiu remot"), + ("remote_restarting_tip", "S'està reiniciant el dispositiu remot. Tanqueu aquest missatge i torneu a connectar amb ell mitjançant la contrasenya, un cop estigui en línia."), + ("Copied", "S'ha copiat"), ("Exit Fullscreen", "Surt de la pantalla completa"), ("Fullscreen", "Pantalla completa"), - ("Mobile Actions", "Accions mòbils"), - ("Select Monitor", "Selecciona el monitor"), - ("Control Actions", "Accions de control"), + ("Mobile Actions", "Funcions mòbils"), + ("Select Monitor", "Selecció de monitor"), + ("Control Actions", "Control de funcions"), ("Display Settings", "Configuració de pantalla"), ("Ratio", "Relació"), - ("Image Quality", "Qualitat d'imatge"), - ("Scroll Style", "Estil de desplaçament"), + ("Image Quality", "Qualitat de la imatge"), + ("Scroll Style", "Tipus de desplaçament"), ("Show Toolbar", "Mostra la barra d'eines"), ("Hide Toolbar", "Amaga la barra d'eines"), ("Direct Connection", "Connexió directa"), - ("Relay Connection", "Connexió Relay"), + ("Relay Connection", "Connexió amb repetidor"), ("Secure Connection", "Connexió segura"), - ("Insecure Connection", "Connexió insegura"), + ("Insecure Connection", "Connexió no segura"), ("Scale original", "Escala original"), ("Scale adaptive", "Escala adaptativa"), ("General", "General"), ("Security", "Seguretat"), ("Theme", "Tema"), - ("Dark Theme", "Tema Fosc"), + ("Dark Theme", "Tema fosc"), ("Light Theme", "Tema clar"), ("Dark", "Fosc"), ("Light", "Clar"), - ("Follow System", "Tema del sistema"), - ("Enable hardware codec", "Habilita el còdec per hardware"), - ("Unlock Security Settings", "Desbloqueja els ajustaments de seguretat"), + ("Follow System", "Utilitza la configuració del sistema"), + ("Enable hardware codec", "Habilita la codificació per maquinari"), + ("Unlock Security Settings", "Desbloca la configuració de seguretat"), ("Enable audio", "Habilita l'àudio"), - ("Unlock Network Settings", "Desbloqueja els ajustaments de xarxa"), + ("Unlock Network Settings", "Desbloca la configuració de la xarxa"), ("Server", "Servidor"), - ("Direct IP Access", "Accés IP Directe"), - ("Proxy", "Proxy"), + ("Direct IP Access", "Accés directe per IP"), + ("Proxy", "Servidor intermediari"), ("Apply", "Aplica"), - ("Disconnect all devices?", "Vols desconnectar tots els dispositius?"), - ("Clear", "Neteja"), + ("Disconnect all devices?", "Voleu desconnectar tots els dispositius?"), + ("Clear", "Buida"), ("Audio Input Device", "Dispositiu d'entrada d'àudio"), - ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), + ("Use IP Whitelisting", "Utilitza un llistat d'IP admeses"), ("Network", "Xarxa"), - ("Pin Toolbar", "Fixa la barra d'eines"), - ("Unpin Toolbar", "Deixa de fixar la barra d'eines"), - ("Recording", "Gravant"), - ("Directory", "Directori"), - ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), + ("Pin Toolbar", "Ancora a la barra d'eines"), + ("Unpin Toolbar", "Desancora de la barra d'eines"), + ("Recording", "Gravació"), + ("Directory", "Contactes"), + ("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"), + ("Automatically record outgoing sessions", ""), ("Change", "Canvia"), - ("Start session recording", "Comença la gravació de la sessió"), + ("Start session recording", "Inicia la gravació de la sessió"), ("Stop session recording", "Atura la gravació de la sessió"), ("Enable recording session", "Habilita la gravació de la sessió"), - ("Enable LAN discovery", "Habilita el descobriment de LAN"), - ("Deny LAN discovery", "Denega el descobriment de LAN"), - ("Write a message", "Escriu un missatge"), - ("Prompt", "Consulta"), - ("Please wait for confirmation of UAC...", "Espera per confirmar l'UAC..."), - ("elevated_foreground_window_tip", ""), + ("Enable LAN discovery", "Habilita el descobriment LAN"), + ("Deny LAN discovery", "Inhabilita el descobriment LAN"), + ("Write a message", "Escriviu un missatge"), + ("Prompt", "Sol·licitud"), + ("Please wait for confirmation of UAC...", "Espereu a la confirmació de l'UAC..."), + ("elevated_foreground_window_tip", "La finestra de connexió actual requereix permisos ampliats per a funcionar i, de forma temporal, no es pot utilitzar ni el teclat ni el ratolí. Demaneu a l'usuari remot que minimitzi la finestra actual, o bé que faci clic al botó Permisos ampliats de la finestra d'administració de la connexió. Per a evitar aquest problema en un futur, instal·leu el RustDesk al dispositiu remot."), ("Disconnected", "Desconnectat"), ("Other", "Altre"), - ("Confirm before closing multiple tabs", "Confirma abans de tancar múltiples pestanyes"), - ("Keyboard Settings", "Ajustaments de teclat"), - ("Full Access", "Acces complet"), - ("Screen Share", "Comparteix la pantalla"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Prova l'escriptori X11 o canvia el teu sistema operatiu."), - ("JumpLink", "Veure"), - ("Please Select the screen to be shared(Operate on the peer side).", "Selecciona la pantalla que es compartirà (Opera al costat del peer)."), - ("Show RustDesk", "Mostra RustDesk"), - ("This PC", "Aquest PC"), + ("Confirm before closing multiple tabs", "Confirma abans de tancar diverses pestanyes alhora"), + ("Keyboard Settings", "Configuració del teclat"), + ("Full Access", "Accés complet"), + ("Screen Share", "Compartició de pantalla"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), + ("JumpLink", "Marcador"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"), + ("Show RustDesk", "Mostra el RustDesk"), + ("This PC", "Aquest equip"), ("or", "o"), - ("Continue with", "Continur amb"), - ("Elevate", "Eleva"), - ("Zoom cursor", "Zoom del ratolí"), - ("Accept sessions via password", "Accepta sessions via contrasenya"), - ("Accept sessions via click", "Accepta sessions via clic"), - ("Accept sessions via both", "Accepta sessions via les dues opcions"), - ("Please wait for the remote side to accept your session request...", "Esperea que la part remota accepti la teva sol·licitud de sessió..."), + ("Continue with", "Continua amb"), + ("Elevate", "Permisos ampliats"), + ("Zoom cursor", "Escala del ratolí"), + ("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"), + ("Accept sessions via click", "Accepta les sessions expressament amb el ratolí"), + ("Accept sessions via both", "Accepta les sessions de totes dues formes"), + ("Please wait for the remote side to accept your session request...", "S'està esperant l'acceptació remota de la vostra connexió..."), ("One-time Password", "Contrasenya d'un sol ús"), - ("Use one-time password", "Fes servir una contrasenya d'un sol ús"), - ("One-time password length", "Caracters de la contrasenya d'un sol ús"), - ("Request access to your device", "Sol·licita l'acces al teu dispositiu"), - ("Hide connection management window", "Amaga la finestra de gestió de connexió"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", "Clic dret per seleccionar les pestanyes"), - ("Skipped", "Saltat"), + ("Use one-time password", "Utilitza una contrasenya d'un sol ús"), + ("One-time password length", "Mida de la contrasenya d'un sol ús"), + ("Request access to your device", "Ha demanat connectar al vostre dispositiu"), + ("Hide connection management window", "Amaga la finestra d'administració de la connexió"), + ("hide_cm_tip", "Permet amagar la finestra només en acceptar sessions entrants sempre que s'utilitzi una contrasenya permanent"), + ("wayland_experiment_tip", "El suport per a Wayland està en fase experimental; es recomana l'ús d'x11 si us cal accés de forma desatesa."), + ("Right click to select tabs", "Feu clic amb el botó dret per a seleccionar pestanyes"), + ("Skipped", "S'ha omès"), ("Add to address book", "Afegeix a la llibreta d'adreces"), ("Group", "Grup"), ("Search", "Cerca"), - ("Closed manually by web console", "Tancat manualment amb la consola web"), + ("Closed manually by web console", "Tancat manualment per la consola web"), ("Local keyboard type", "Tipus de teclat local"), - ("Select local keyboard type", "Selecciona el tipus de teclat local"), - ("software_render_tip", ""), - ("Always use software rendering", "Fes servir sempre la renderització per software"), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", "Espera"), - ("Elevation Error", "Error d'elevació"), - ("Ask the remote user for authentication", "Demana autenticació a l'usuari remot"), - ("Choose this if the remote account is administrator", "Selecciona això si l'usuari remot és administrador"), - ("Transmit the username and password of administrator", "Transmet el nom d'usuari i la contrasenya de l'administrador"), - ("still_click_uac_tip", ""), - ("Request Elevation", "Demana l'elevació"), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", "Elevació exitosa"), + ("Select local keyboard type", "Seleccioneu el tipus de teclat local"), + ("software_render_tip", "Si utilitzeu una gràfica Nvidia a Linux i la connexió remota es tanca immediatament en connectar, canviar al controlador lliure «Nouveau» amb renderització per programari, pot ajudar a solucionar el problema. Es requerirà en aquest cas reiniciar l'aplicació."), + ("Always use software rendering", "Utilitza sempre la renderització de programari"), + ("config_input", "Per a poder controlar el dispositiu remotament amb el teclat, faciliteu al RustDesk els permisos d'entrada necessaris."), + ("config_microphone", "Per a poder parlar remotament, faciliteu al RustDesk els permisos de gravació d'àudio necessaris."), + ("request_elevation_tip", "També, la part remota pot concedir aquests permisos de forma manual."), + ("Wait", "Espereu"), + ("Elevation Error", "Error de permisos"), + ("Ask the remote user for authentication", "Demaneu l'autenticació al client remot"), + ("Choose this if the remote account is administrator", "Trieu aquesta opció si el compte remot té permisos d'administrador"), + ("Transmit the username and password of administrator", "Indiqueu l'usuari i contrasenya de l'administrador"), + ("still_click_uac_tip", "Es requereix acceptació manual a la part remota de la finestra «UAC» del RustDesk en execució."), + ("Request Elevation", "Sol·licita els permisos"), + ("wait_accept_uac_tip", "Espereu fins que l'usuari remot accepti la finestra de diàleg de l'«UAC»."), + ("Elevate successfully", "S'han acceptat els permisos"), ("uppercase", "majúscula"), ("lowercase", "minúscula"), - ("digit", "dígit"), + ("digit", "número"), ("special character", "caràcter especial"), - ("length>=8", "longitut>=8"), - ("Weak", "Dèbil"), - ("Medium", "Mitja"), - ("Strong", "Forta"), - ("Switch Sides", "Canvia de costat"), - ("Please confirm if you want to share your desktop?", "Confirma que vols compartir el teu escriptori?"), + ("length>=8", "mida>=8"), + ("Weak", "Feble"), + ("Medium", "Acceptable"), + ("Strong", "Segura"), + ("Switch Sides", "Inverteix la connexió"), + ("Please confirm if you want to share your desktop?", "Realment voleu que es controli aquest equip?"), ("Display", "Pantalla"), - ("Default View Style", "Estil de visualització predeterminat"), - ("Default Scroll Style", "Estil de desplaçament predeterminat"), - ("Default Image Quality", "Qualitat d'imatge predeterminada"), - ("Default Codec", "Còdec predeterminat"), - ("Bitrate", "Ratio de bits"), + ("Default View Style", "Estil de vista per defecte"), + ("Default Scroll Style", "Estil de desplaçament per defecte"), + ("Default Image Quality", "Qualitat de la imatge per defecte"), + ("Default Codec", "Còdec per defecte"), + ("Bitrate", "Taxa de bits"), ("FPS", "FPS"), - ("Auto", "Auto"), - ("Other Default Options", "Altres opcions predeterminades"), - ("Voice call", "Trucada de veu"), - ("Text chat", "Xat de text"), - ("Stop voice call", "Penja la trucada de veu"), - ("relay_hint_tip", ""), - ("Reconnect", "Reconecta"), - ("Codec", "Còdec"), + ("Auto", "Automàtic"), + ("Other Default Options", "Altres opcions per defecte"), + ("Voice call", "Trucada"), + ("Text chat", "Xat"), + ("Stop voice call", "Penja la trucada"), + ("relay_hint_tip", "Quan no sigui possible la connexió directa, podeu provar mitjançant un repetidor. Addicionalment, si voleu que l'ús d'un repetidor sigui la primera opció per defecte, podeu afegir el sufix «/r» a la ID, o seleccionar l'opció «Connecta sempre mitjançant un repetidor» si ja existeix una fitxa amb aquesta ID a la pestanya de connexions recents."), + ("Reconnect", "Torna a connectar"), + ("Codec", "Còdec"), ("Resolution", "Resolució"), - ("No transfers in progress", "Sense transferències en curs"), - ("Set one-time password length", "Selecciona la longitud de la contrasenya d'un sol ús"), - ("RDP Settings", "Configuració RDP"), - ("Sort by", "Ordena per"), - ("New Connection", "Nova connexió"), + ("No transfers in progress", "Cap transferència iniciada"), + ("Set one-time password length", "Mida de la contrasenya d'un sol ús"), + ("RDP Settings", "Opcions de connexió RDP"), + ("Sort by", "Organitza per"), + ("New Connection", "Connexió nova"), ("Restore", "Restaura"), ("Minimize", "Minimitza"), - ("Maximize", "Maximtiza"), - ("Your Device", "El teu dispositiu"), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", "p.ex.: admin"), - ("Empty Username", "Usuari buit"), + ("Maximize", "Maximitza"), + ("Your Device", "Aquest dispositiu"), + ("empty_recent_tip", "No s'ha trobat cap sessió recent!\nS'afegiran automàticament les connexions que realitzeu."), + ("empty_favorite_tip", "No heu afegit cap dispositiu aquí!\nPodeu afegir dispositius favorits en qualsevol moment."), + ("empty_lan_tip", "No s'ha trobat cap dispositiu proper."), + ("empty_address_book_tip", "Sembla que no teniu cap dispositiu a la vostra llista d'adreces."), + ("eg: admin", "p. ex.:admin"), + ("Empty Username", "Nom d'usuari buit"), ("Empty Password", "Contrasenya buida"), - ("Me", "Jo"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", "Tipus de visualització"), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", "No cal elevar permisos"), - ("System Sound", "Sistema de so"), - ("Default", "Predeterminat"), - ("New RDP", "Nou RDP"), - ("Fingerprint", "Empremta digital"), - ("Copy Fingerprint", "Copia l'emprenta digital"), - ("no fingerprints", "sense emprentes"), - ("Select a peer", "Selecciona un peer"), - ("Select peers", "Selecciona diversos peers"), + ("Me", "Vós"), + ("identical_file_tip", "Aquest fitxer és idèntic al del client."), + ("show_monitors_tip", "Mostra les pantalles a la barra d'eines"), + ("View Mode", "Mode espectador"), + ("login_linux_tip", "És necessari que inicieu prèviament sessió amb un entorn d'escriptori x11 habilitat"), + ("verify_rustdesk_password_tip", "Verifica la contrasenya del RustDesk"), + ("remember_account_tip", "Recorda aquest compte"), + ("os_account_desk_tip", "S'utilitza aquest compte per iniciar la sessió al sistema remot i habilitar el mode sense cap pantalla connectada"), + ("OS Account", "Compte d'usuari"), + ("another_user_login_title_tip", "Altre usuari ha iniciat ja una sessió"), + ("another_user_login_text_tip", "Desconnecta"), + ("xorg_not_found_title_tip", "No s'ha trobat l'entorn Xorg"), + ("xorg_not_found_text_tip", "Instal·leu el Xorg"), + ("no_desktop_title_tip", "Cap escriptori disponible"), + ("no_desktop_text_tip", "Instal·leu l'entorn d'escriptori GNOME"), + ("No need to elevate", "No calen permisos ampliats"), + ("System Sound", "So del sistema"), + ("Default", "per defecte"), + ("New RDP", "Connexió RDP nova"), + ("Fingerprint", "Empremta"), + ("Copy Fingerprint", "Copia l'empremta"), + ("no fingerprints", "Cap empremta"), + ("Select a peer", "Seleccioneu un client"), + ("Select peers", "Seleccioneu els clients"), ("Plugins", "Complements"), ("Uninstall", "Desinstal·la"), ("Update", "Actualitza"), ("Enable", "Activa"), ("Disable", "Desactiva"), ("Options", "Opcions"), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", "Col·lapsa la barra d'etiquetes"), - ("Accept and Elevate", "Accepta i eleva"), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), + ("resolution_original_tip", "Resolució original"), + ("resolution_fit_local_tip", "Ajusta la resolució local"), + ("resolution_custom_tip", "Resolució personalitzada"), + ("Collapse toolbar", "Minimitza la barra d'eines"), + ("Accept and Elevate", "Accepta i permet"), + ("accept_and_elevate_btn_tooltip", "Accepta la connexió i permet els permisos elevats UAC."), + ("clipboard_wait_response_timeout_tip", "S'ha esgotat el temps d'espera amb la resposta de còpia."), ("Incoming connection", "Connexió entrant"), ("Outgoing connection", "Connexió sortint"), - ("Exit", "Tanca"), + ("Exit", "Surt"), ("Open", "Obre"), - ("logout_tip", ""), + ("logout_tip", "Segur que voleu desconnectar?"), ("Service", "Servei"), ("Start", "Inicia"), ("Stop", "Atura"), - ("exceed_max_devices", ""), + ("exceed_max_devices", "Heu assolit el nombre màxim de dispositius administrables."), ("Sync with recent sessions", "Sincronitza amb les sessions recents"), - ("Sort tags", "Ordena per etiquetes"), - ("Open connection in new tab", "Obre la connexió en una nova pestanya"), - ("Move tab to new window", "Mou la pestanya a una nova finestra"), + ("Sort tags", "Ordena les etiquetes"), + ("Open connection in new tab", "Obre la connexió en una pestanya nova"), + ("Move tab to new window", "Mou la pestanya a una finestra nova"), ("Can not be empty", "No pot estar buit"), ("Already exists", "Ja existeix"), ("Change Password", "Canvia la contrasenya"), - ("Refresh Password", "Refresca la contrasenya"), + ("Refresh Password", "Actualitza la contrasenya"), ("ID", "ID"), - ("Grid View", "Visualització de graella"), - ("List View", "Visualització de llista"), + ("Grid View", "Disposició de graella"), + ("List View", "Disposició de llista"), ("Select", "Selecciona"), - ("Toggle Tags", "Activa/desactiva les etiquetes"), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), + ("Toggle Tags", "Habilita les etiquetes"), + ("pull_ab_failed_tip", "Ha fallat en actualitzar la llista de contactes"), + ("push_ab_failed_tip", "Ha fallat en actualitzar la llista amb el servidor"), + ("synced_peer_readded_tip", "Els dispositius que es troben a la llista de sessions recents se sincronitzaran novament a la llista de contactes."), ("Change Color", "Canvia el color"), - ("Primary Color", "Color primari"), + ("Primary Color", "Color principal"), ("HSV Color", "Color HSV"), - ("Installation Successful!", "Instal·lació correcta!"), - ("Installation failed!", "Ha fallat la instal·lació!"), - ("Reverse mouse wheel", "Canvia l'orientació de la roda del ratolí"), + ("Installation Successful!", "S'ha instal·lat correctament"), + ("Installation failed!", "Ha fallat la instal·lació"), + ("Reverse mouse wheel", "Inverteix la roda del ratolí"), ("{} sessions", "{} sessions"), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", "No ho mostris més"), - ("I Agree", "Accepta"), - ("Decline", "Rebutja"), - ("Timeout in minutes", "Desconexió en minuts"), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", "Connexió fallada per inactivitat"), - ("Check for software update on startup", "Revisa les actualitzacions de software en iniciar"), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), + ("scam_title", "Podríeu ser víctima d'una ESTAFA!"), + ("scam_text1", "Si cap persona qui NO coneixeu NI CONFIEU us demanés l'ús del RustDesk, no continueu i talleu la comunicació immediatament."), + ("scam_text2", "Habitualment solen ser atacants intentant fer-se amb els vostres diners o informació privada."), + ("Don't show again", "No tornis a mostrar"), + ("I Agree", "Accepto"), + ("Decline", "No accepto"), + ("Timeout in minutes", "Temps d'espera en minuts"), + ("auto_disconnect_option_tip", "Tanca automàticament les sessions entrants per inactivitat de l'usuari"), + ("Connection failed due to inactivity", "Ha fallat la connexió per inactivitat"), + ("Check for software update on startup", "Cerca actualitzacions en iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Actualitzeu el RustDesk Server Pro a la versió {} o superior!"), + ("pull_group_failed_tip", "Ha fallat en actualitzar el grup"), ("Filter by intersection", "Filtra per intersecció"), - ("Remove wallpaper during incoming sessions", "Amaga el fons de pantalla en les connexions entrants"), + ("Remove wallpaper during incoming sessions", "Inhabilita el fons d'escriptori durant la sessió entrant"), ("Test", "Prova"), - ("display_is_plugged_out_msg", ""), - ("No displays", "Sense pantalles"), - ("Open in new window", "Obre en una nova finestra"), - ("Show displays as individual windows", "Mostra les pantalles com finestres individuals"), - ("Use all my displays for the remote session", "Fes servir totes les meves pantalles per la sessió remota"), - ("selinux_tip", ""), + ("display_is_plugged_out_msg", "El monitor està desconnectat; canvieu primer al monitor principal."), + ("No displays", "Cap monitor"), + ("Open in new window", "Obre en una finestra nova"), + ("Show displays as individual windows", "Mostra cada monitor com una finestra individual"), + ("Use all my displays for the remote session", "Utilitza tots els meus monitors per a la connexió remota"), + ("selinux_tip", "SELinux està activat al vostre dispositiu, la qual cosa evita que el RustDesk funcioni correctament com a equip controlable."), ("Change view", "Canvia la vista"), - ("Big tiles", "Títols grans"), - ("Small tiles", "Títols petits"), + ("Big tiles", "Mosaic gran"), + ("Small tiles", "Mosaic petit"), ("List", "Llista"), ("Virtual display", "Pantalla virtual"), - ("Plug out all", "Desconnectar tots"), + ("Plug out all", "Desconnecta-ho tot"), ("True color (4:4:4)", "Color real (4:4:4)"), - ("Enable blocking user input", "Activa el bloqueig d'entrada d'usuari"), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", "Entra al mode de privacitat"), - ("Exit privacy mode", "Surt del mode de privacitat"), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", "Canvia la tecla de control"), - ("swap-left-right-mouse", ""), + ("Enable blocking user input", "Bloca el control de l'usuari amb els dispositius d'entrada"), + ("id_input_tip", "Evita que l'usuari pugui interactuar p. ex. amb el teclat o ratolí"), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Inicia el Mode privat"), + ("Exit privacy mode", "Surt del Mode privat"), + ("idd_not_support_under_win10_2004_tip", "El controlador indirecte de pantalla no està suportat; es requereix Windows 10 versió 2004 o superior."), + ("input_source_1_tip", "Font d'entrada 1"), + ("input_source_2_tip", "Font d'entrada 2"), + ("Swap control-command key", "Canvia el comportament de la tecla Control"), + ("swap-left-right-mouse", "Alterna el comportament dels botons esquerre-dret del ratolí"), ("2FA code", "Codi 2FA"), ("More", "Més"), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", "El codi de verificació de l'email ha de tenir 6 caràcters."), - ("2FA code must be 6 digits.", "El codi 2FA ha de tenir 6 digits."), - ("Multiple Windows sessions found", "Multiples sessions de Windows trobades"), - ("Please select the session you want to connect to", "Selecciona la sessió a la què et vols connectar"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), + ("enable-2fa-title", "Habilita el mètode d'autenticació de factor doble"), + ("enable-2fa-desc", "Configureu ara el vostre autenticador. Podeu utilitzar una aplicació com 2fast, FreeOTP, MultiOTP, Microsoft o Google Authenticator al vostre telèfon o escriptori.\n\nEscanegeu el codi QR amb l'aplicació i escriviu els caràcters resultants per habilitar l'autenticació de factor doble."), + ("wrong-2fa-code", "Codi 2FA no vàlid. Verifiqueu el que heu escrit i també que la configuració horària sigui correcta"), + ("enter-2fa-title", "Autenticació de factor doble"), + ("Email verification code must be 6 characters.", "El codi de verificació de correu-e són 6 caràcters"), + ("2FA code must be 6 digits.", "El codi de verificació 2FA haurien de ser almenys 6 dígits"), + ("Multiple Windows sessions found", "S'han trobat múltiples sessions en ús del Windows"), + ("Please select the session you want to connect to", "Indiqueu amb quina sessió voleu connectar"), + ("powered_by_me", "Amb la tecnologia de RustDesk"), + ("outgoing_only_desk_tip", "Aquesta és una versió personalitzada.\nPodeu connectar amb altres dispositius, però no s'accepten connexions d'entrada cap el vostre dispositiu."), + ("preset_password_warning", "Aquesta versió personalitzada té una contrasenya preestablerta. Qualsevol persona que la conegui pot tenir accés total al vostre dispositiu. Si no és el comportament desitjat, desinstal·leu aquest programa immediatament."), ("Security Alert", "Alerta de seguretat"), - ("My address book", "La meva llibreta d'adreces"), + ("My address book", "Llibreta d'adreces"), ("Personal", "Personal"), ("Owner", "Propietari"), - ("Set shared password", "Establir la contrasenya compartida"), - ("Exist in", "Existeix en"), + ("Set shared password", "Establiu una contrasenya compartida"), + ("Exist in", "Existeix a"), ("Read-only", "Només lectura"), ("Read/Write", "Lectura/Escriptura"), ("Full Control", "Control total"), - ("share_warning_tip", ""), - ("Everyone", "Tots"), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", "Segueix el cursor remot"), - ("Follow remote window focus", "Segueix el focus de la finestra remota"), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("share_warning_tip", "Els camps a continuació estan compartits i són visibles a d'altres."), + ("Everyone", "Tothom"), + ("ab_web_console_tip", "Més a la consola web"), + ("allow-only-conn-window-open-tip", "Permet la connexió només si la finestra del RustDesk està activa"), + ("no_need_privacy_mode_no_physical_displays_tip", "Cap monitor físic. No cal l'ús del Mode privat"), + ("Follow remote cursor", "Segueix al cursor remot"), + ("Follow remote window focus", "Segueix el focus remot de la finestra activa"), + ("default_proxy_tip", "El protocol per defecte és Socks5 al port 1080"), + ("no_audio_input_device_tip", "No s'ha trobat cap dispositiu d'àudio."), ("Incoming", "Entrant"), ("Outgoing", "Sortint"), - ("Clear Wayland screen selection", "Neteja la selecció de pantalla Wayland"), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", "Fes servir la renderització de textures"), + ("Clear Wayland screen selection", "Neteja la pantalla de selecció Wayland"), + ("clear_Wayland_screen_selection_tip", "En netejar la finestra de selecció, podreu tornar a triar quina pantalla compartir."), + ("confirm_clear_Wayland_screen_selection_tip", "Segur que voleu netejar la pantalla de selecció del Wayland"), + ("android_new_voice_call_tip", "S'ha rebut una petició de trucada entrant. Si accepteu, la font d'àudio canviarà a comunicació per veu."), + ("texture_render_tip", "Utilitzeu aquesta opció per suavitzar la imatge. Desactiveu-ho si trobeu cap problema amb el renderitzat"), + ("Use texture rendering", "Utilitza la renderització de textures"), ("Floating window", "Finestra flotant"), - ("floating_window_tip", ""), - ("Keep screen on", "Deixa la pantalla encesa"), + ("floating_window_tip", "Ajuda a mantenir el servei del RustDesk en rerefons"), + ("Keep screen on", "Manté la pantalla activa"), ("Never", "Mai"), - ("During controlled", "Mentre estigui controlat"), - ("During service is on", "Mentre el servei estigui encés"), - ("Capture screen using DirectX", "Captura de pantalla utilitzant DirectX"), + ("During controlled", "Durant la connexió"), + ("During service is on", "Mentre el servei està actiu"), + ("Capture screen using DirectX", "Captura utilitzant el DirectX"), ("Back", "Enrere"), ("Apps", "Aplicacions"), - ("Volume up", "Puja el volum"), - ("Volume down", "Baixa el volum"), - ("Power", "Engega"), - ("Telegram bot", "Bot de Telegram"), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Volume up", "Volum amunt"), + ("Volume down", "Volum avall"), + ("Power", "Encesa"), + ("Telegram bot", "Bot del Telegram"), + ("enable-bot-tip", "Si habiliteu aquesta característica, podreu rebre el codi 2FA mitjançant el vostre bot. També funciona com a notificador de la connexió."), + ("enable-bot-desc", "1. Obriu un xat amb @BotFather.\n2. Envieu l'ordre \"/newbot\". Rebreu un testimoni en acompletar aquest pas.\n3. Inicieu una conversa amb el vostre bot nou que acabeu de crear, enviant un missatge que comenci amb (\"/\"), com ara \"/hello\" per a activar-lo.\n"), + ("cancel-2fa-confirm-tip", "Segur que voleu cancel·lar l'autenticació 2FA?"), + ("cancel-bot-confirm-tip", "Segur que voleu cancel·lar el bot de Telegram?"), + ("About RustDesk", "Quant al RustDesk"), + ("Send clipboard keystrokes", "Envia les pulsacions de tecles del porta-retalls"), + ("network_error_tip", "Verifiqueu la vostra connexió a Internet i torneu a provar"), + ("Unlock with PIN", "Desbloca amb PIN"), + ("Requires at least {} characters", "Són necessaris almenys {} caràcters"), + ("Wrong PIN", "PIN no vàlid"), + ("Set PIN", "Definiu un codi PIN"), + ("Enable trusted devices", "Habilita els dispositius de confiança"), + ("Manage trusted devices", "Administra els dispositius de confiança"), + ("Platform", "Platforma"), + ("Days remaining", "Dies restants"), + ("enable-trusted-devices-tip", "Omet l'autenticació de factor doble (2FA) als dispositius de confiança"), + ("Parent directory", "Carpeta pare"), + ("Resume", "Continua"), + ("Invalid file name", "Nom de fitxer no vàlid"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("Authentication Required", "Autenticació requerida"), + ("Authenticate", "Autentica"), + ("web_id_input_tip", "Podeu inserir el número ID al propi servidor; l'accés directe per IP no és compatible amb el client web.\nSi voleu accedir a un dispositiu d'un altre servidor, afegiu l'adreça del servidor, com ara @?key= (p. ex.\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi voleu accedir a un dispositiu en un servidor públic, no cal que inseriu la clau pública «@» per al servidor públic."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 62cb5452c60..8b1d3a5f9a4 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "显示质量监测"), ("Disable clipboard", "禁用剪贴板"), ("Lock after session end", "会话结束后锁定远程电脑"), - ("Insert", "插入"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), ("ID does not exist", "ID 不存在"), @@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1 传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -363,7 +363,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "取消固定工具栏"), ("Recording", "录屏"), ("Directory", "目录"), - ("Automatically record incoming sessions", "自动录制来访会话"), + ("Automatically record incoming sessions", "自动录制传入会话"), + ("Automatically record outgoing sessions", "自动录制传出会话"), ("Change", "更改"), ("Start session recording", "开始录屏"), ("Stop session recording", "结束录屏"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "拔出所有"), ("True color (4:4:4)", "真彩模式(4:4:4)"), ("Enable blocking user input", "允许阻止用户输入"), - ("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\", 无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"), ("privacy_mode_impl_mag_tip", "模式 1"), ("privacy_mode_impl_virtual_display_tip", "模式 2"), ("Enter privacy mode", "进入隐私模式"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "父目录"), ("Resume", "继续"), ("Invalid file name", "无效文件名"), + ("one-way-file-transfer-tip", "被控端启用了单向文件传输"), + ("Authentication Required", "需要身份验证"), + ("Authenticate", "认证"), + ("web_id_input_tip", "可以输入同一个服务器内的 ID,web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。"), + ("Download", "下载"), + ("Upload folder", "上传文件夹"), + ("Upload files", "上传文件"), + ("Clipboard is synchronized", "剪贴板已同步"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 41d94d97846..a9fb5b233cb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Zobrazit monitor kvality"), ("Disable clipboard", "Vypnout schránku"), ("Lock after session end", "Po ukončení relace zamknout plochu"), - ("Insert", "Vložit"), + ("Insert Ctrl + Alt + Del", "Vložit Ctrl + Alt + Del"), ("Insert Lock", "Zamknout"), ("Refresh", "Načíst znovu"), ("ID does not exist", "Toto ID neexistuje"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nahrávání"), ("Directory", "Adresář"), ("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"), + ("Automatically record outgoing sessions", ""), ("Change", "Změnit"), ("Start session recording", "Spustit záznam relace"), ("Stop session recording", "Zastavit záznam relace"), @@ -641,8 +642,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Platforma"), ("Days remaining", "Zbývajících dnů"), ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "Rodičovský adresář"), + ("Resume", "Pokračovat"), + ("Invalid file name", "Nesprávný název souboru"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index a9e286600b6..34e5433f517 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Dit skrivebord"), - ("desk_tip", "Du kan få adgang til dit skrivebord med dette ID og adgangskode."), + ("desk_tip", "Du kan give adgang til dit skrivebord med dette ID og denne adgangskode."), ("Password", "Adgangskode"), ("Ready", "Klar"), ("Established", "Etableret"), @@ -38,18 +38,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Sluk for forbindelsesserveren"), ("Change ID", "Ændr ID"), ("Your new ID", "Dit nye ID"), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), + ("length %min% to %max%", "længde %min% til %max%"), + ("starts with a letter", "starter med ét bogstav"), + ("allowed characters", "tilladte tegn"), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Antal tegn skal være mellem 6 og 16."), ("Website", "Hjemmeside"), ("About", "Om"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"), + ("Privacy Statement", "Privatlivspolitik"), ("Mute", "Sluk for mikrofonen"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Build dato"), + ("Version", "Version"), + ("Home", "Hjem"), ("Audio Input", "Lydinput"), ("Enhancements", "Forbedringer"), ("Hardware Codec", "Hardware-codec"), @@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Stræk ud"), - ("Scrollbar", "Rullebar"), - ("ScrollAuto", "Auto-rul"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "Auto-scroll"), ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Vis billedkvalitet"), ("Disable clipboard", "Deaktiver udklipsholder"), ("Lock after session end", "Lås efter afslutningen af fjernstyring"), - ("Insert", "Indsæt"), + ("Insert Ctrl + Alt + Del", "Indsæt Ctrl + Alt + Del"), ("Insert Lock", "Indsæt lås"), ("Refresh", "Genopfrisk"), ("ID does not exist", "ID findes ikke"), @@ -139,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Fjernskrivebord er offline"), ("Key mismatch", "Nøgle uoverensstemmelse"), ("Timeout", "Timeout"), - ("Failed to connect to relay server", "Forbindelse til relæ-serveren mislykkedes"), + ("Failed to connect to relay server", "Forbindelse til relay-serveren mislykkedes"), ("Failed to connect via rendezvous server", "Forbindelse via Rendezvous-server mislykkedes"), - ("Failed to connect via relay server", "Forbindelse via relæ-serveren mislykkedes"), + ("Failed to connect via relay server", "Forbindelse via relay-serveren mislykkedes"), ("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"), ("Set Password", "Indstil adgangskode"), ("OS Password", "Operativsystemadgangskode"), @@ -218,7 +218,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember me", "Husk mig"), ("Trust this device", "Husk denne enhed"), ("Verification code", "Verifikationskode"), - ("verification_tip", ""), + ("verification_tip", "En bekræftelseskode er blevet sendt til den registrerede e-mail adresse. Indtast bekræftelseskoden for at logge på."), ("Logout", "Logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg efter ID"), @@ -230,7 +230,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Glemt brugernavn"), ("Password missed", "Glemt kodeord"), ("Wrong credentials", "Forkerte registreringsdata"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Bekræftelsesnøglen er forkert eller er udløbet"), ("Edit Tag", "Rediger nøgleord"), ("Forget Password", "Glem adgangskoden"), ("Favorites", "Favoritter"), @@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"), ("Download new version", "Download ny version"), ("Touch mode", "Touch-tilstand"), - ("Mouse mode", "Musse-tilstand"), + ("Mouse mode", "Muse-tilstand"), ("One-Finger Tap", "En-finger-tryk"), ("Left Mouse", "Venstre mus"), ("One-Long Tap", "Tryk og hold med en finger"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Ved at tænde for skærmoptagelsen startes tjenesten automatisk, så andre enheder kan anmode om en forbindelse fra denne enhed."), ("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."), ("android_version_audio_tip", "Den aktuelle Android-version understøtter ikke lydoptagelse. Android 10 eller højere er påkrævet."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Tryk [Start tjeneste] eller aktivér [Skærmoptagelse] tilladelse for at dele skærmen."), + ("android_permission_may_not_change_tip", "Rettigheder til oprettede forbindelser ændres ikke med det samme før der forbindelsen genoprettes."), ("Account", "Konto"), ("Overwrite", "Overskriv"), ("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"), @@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Sprog"), ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), ("Ignore Battery Optimizations", "Ignorér betteri optimeringer"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Hvis du ønsker at slukke for denne funktion, åbn RustDesk appens indstillinger, tryk på [Batteri], og fjern flueben ved [Uden begrænsninger]"), ("Start on boot", "Start under opstart"), ("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"), ("Connection not allowed", "Forbindelse ikke tilladt"), @@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Map mode", "Kortmodus"), ("Translate mode", "Oversættelsesmodus"), ("Use permanent password", "Brug permanent adgangskode"), - ("Use both passwords", "Brug begge adgangskoder"), + ("Use both passwords", "Brug begge typer adgangskoder"), ("Set permanent password", "Sæt permanent adgangskode"), ("Enable remote restart", "Aktivér fjerngenstart"), ("Restart remote device", "Genstart fjernenhed"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Forhold"), ("Image Quality", "Billedkvalitet"), ("Scroll Style", "Rullestil"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Vis værktøjslinje"), + ("Hide Toolbar", "Skjul værktøjslinje"), ("Direct Connection", "Direkte forbindelse"), ("Relay Connection", "Viderestillingsforbindelse"), ("Secure Connection", "Sikker forbindelse"), @@ -359,24 +359,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Lydindgangsenhed"), ("Use IP Whitelisting", "Brug IP Whitelisting"), ("Network", "Netværk"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Fastgør værktøjslinjen"), + ("Unpin Toolbar", "Frigiv værktøjslinjen"), ("Recording", "Optager"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), + ("Automatically record outgoing sessions", ""), ("Change", "Ændr"), ("Start session recording", "Start sessionsoptagelse"), ("Stop session recording", "Stop sessionsoptagelse"), ("Enable recording session", "Aktivér optagelsessession"), - ("Enable LAN discovery", "Aktivér LAN Discovery"), - ("Deny LAN discovery", "Afvis LAN Discovery"), + ("Enable LAN discovery", "Aktivér opdagelse via det lokale netværk"), + ("Deny LAN discovery", "Afvis opdagelse via det lokale netværk"), ("Write a message", "Skriv en besked"), ("Prompt", "Prompt"), ("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."), - ("elevated_foreground_window_tip", ""), + ("elevated_foreground_window_tip", "Det nuværende vindue på fjernskrivebordet kræver højere rettigheder for at køre, så det er midlertidigt ikke muligt at bruge musen og tastaturet. Du kan bede fjernbrugeren om at minimere vinduet, eller trykke på elevér knappen i forbindelsesvinduet. For at undgå dette problem, er det anbefalet at installere RustDesk på fjernenheden."), ("Disconnected", "Afbrudt"), ("Other", "Andre"), - ("Confirm before closing multiple tabs", "Bekræft før du lukker flere faner"), + ("Confirm before closing multiple tabs", "Bekræft nedlukning hvis der er flere faner"), ("Keyboard Settings", "Tastaturindstillinger"), ("Full Access", "Fuld adgang"), ("Screen Share", "Skærmdeling"), @@ -399,8 +400,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Engangskode længde"), ("Request access to your device", "Efterspørg adgang til din enhed"), ("Hide connection management window", "Skjul forbindelseshåndteringsvindue"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), + ("hide_cm_tip", "Tillad at skjule, hvis der kun forbindes ved brug af midlertidige og permanente adgangskoder"), + ("wayland_experiment_tip", "Wayland understøttelse er stadigvæk under udvikling. Hvis du har brug for ubemandet adgang, bedes du bruge X11."), ("Right click to select tabs", "Højreklik for at vælge faner"), ("Skipped", "Sprunget over"), ("Add to address book", "Tilføj til adressebog"), @@ -409,19 +410,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Lukket ned manuelt af webkonsollen"), ("Local keyboard type", "Lokal tastatur type"), ("Select local keyboard type", "Vælg lokal tastatur type"), - ("software_render_tip", ""), + ("software_render_tip", "Hvis du bruger et Nvidia grafikkort på Linux, og fjernskrivebordsvinduet lukker ned med det samme efter forbindelsen er oprettet, kan det hjælpe at skifte til Nouveau open-source driveren, og aktivere software rendering. Et genstart af RustDesk er nødvendigt."), ("Always use software rendering", "Brug altid software rendering"), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), + ("config_input", "For at styre fjernskrivebordet med tastaturet, skal du give Rustdesk rettigheder til at optage tastetryk"), + ("config_microphone", "For at tale sammen over fjernstyring, skal du give RustDesk rettigheder til at optage lyd"), + ("request_elevation_tip", "Du kan også spørge om elevationsrettigheder, hvis der er nogen i nærheden af fjernenheden."), ("Wait", "Vent"), ("Elevation Error", "Elevationsfejl"), ("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"), ("Choose this if the remote account is administrator", "Vælg dette hvis fjernbrugeren er en administrator"), ("Transmit the username and password of administrator", "Send brugernavnet og adgangskoden på administratoren"), - ("still_click_uac_tip", ""), + ("still_click_uac_tip", "Kræver stadigvæk at fjernbrugeren skal trykke OK på UAC vinduet ved kørsel af RustDesk."), ("Request Elevation", "Efterspørger elevation"), - ("wait_accept_uac_tip", ""), + ("wait_accept_uac_tip", "Vent venligst på at fjernbrugeren accepterer UAC dialog forespørgslen."), ("Elevate successfully", "Elevation lykkedes"), ("uppercase", "store bogstaver"), ("lowercase", "små bogstaver"), @@ -435,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"), ("Display", "Visning"), ("Default View Style", "Standard visningsstil"), - ("Default Scroll Style", "Standard rulle stil"), + ("Default Scroll Style", "Standard scrollestil"), ("Default Image Quality", "Standard billedkvalitet"), ("Default Codec", "Standard codec"), ("Bitrate", "Bitrate"), @@ -445,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Stemmeopkald"), ("Text chat", "Tekstchat"), ("Stop voice call", "Stop stemmeopkald"), - ("relay_hint_tip", ""), + ("relay_hint_tip", "Det kan ske, at det ikke er muligt at forbinde direkte; du kan forsøge at forbinde via en relay-server. Derudover, hvis du ønsker at bruge en relay-server på dit første forsøg, kan du tilføje \"/r\" efter ID'et, eller bruge valgmuligheden \"Forbind altid via relay-server\" i fanen for seneste sessioner, hvis den findes."), ("Reconnect", "Genopret"), ("Codec", "Codec"), ("Resolution", "Opløsning"), @@ -458,191 +459,199 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimér"), ("Maximize", "Maksimér"), ("Your Device", "Din enhed"), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), + ("empty_recent_tip", "Ups, ingen seneste sessioner!\nTid til at oprette en ny."), + ("empty_favorite_tip", "Ingen yndlings modparter endnu?\nLad os finde én at forbinde til, og tilføje den til dine favoritter!"), + ("empty_lan_tip", "Åh nej, det ser ud til, at vi ikke kunne finde nogen modparter endnu."), + ("empty_address_book_tip", "Åh nej, det ser ud til at der ikke er nogle modparter der er tilføjet til din adressebog."), ("eg: admin", "fx: admin"), ("Empty Username", "Tom brugernavn"), ("Empty Password", "Tom adgangskode"), ("Me", "Mig"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), - ("logout_tip", ""), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("identical_file_tip", "Denne fil er identisk med modpartens."), + ("show_monitors_tip", "Vis skærme i værktøjsbjælken"), + ("View Mode", "Visningstilstand"), + ("login_linux_tip", "Du skal logge på en fjernstyret Linux konto for at aktivere en X skrivebordssession"), + ("verify_rustdesk_password_tip", "Bekræft RustDesk adgangskode"), + ("remember_account_tip", "Husk denne konto"), + ("os_account_desk_tip", "Denne konto benyttes til at logge på fjernsystemet, og aktivere skrivebordssessionen i hovedløs tilstand"), + ("OS Account", "Styresystem konto"), + ("another_user_login_title_tip", "En anden bruger er allerede logget ind"), + ("another_user_login_text_tip", "Frakobl"), + ("xorg_not_found_title_tip", "Xorg ikke fundet"), + ("xorg_not_found_text_tip", "Installér venlist Xorg"), + ("no_desktop_title_tip", "Intet skrivebordsmiljø er tilgængeligt"), + ("no_desktop_text_tip", "Installér venligst GNOME skrivebordet"), + ("No need to elevate", "Ingen grund til at elevere"), + ("System Sound", "Systemlyd"), + ("Default", "Standard"), + ("New RDP", "Ny RDP"), + ("Fingerprint", "Fingeraftryk"), + ("Copy Fingerprint", "Kopiér fingeraftryk"), + ("no fingerprints", "Ingen fingeraftryk"), + ("Select a peer", "Vælg en peer"), + ("Select peers", "Vælg peers"), + ("Plugins", "Plugins"), + ("Uninstall", "Afinstallér"), + ("Update", "Opdatér"), + ("Enable", "Aktivér"), + ("Disable", "Deaktivér"), + ("Options", "Valgmuligheder"), + ("resolution_original_tip", "Original skærmopløsning"), + ("resolution_fit_local_tip", "Tilpas lokal skærmopløsning"), + ("resolution_custom_tip", "Bruger-tilpasset skærmopløsning"), + ("Collapse toolbar", "Skjul værktøjsbjælke"), + ("Accept and Elevate", "Acceptér og elevér"), + ("accept_and_elevate_btn_tooltip", "Acceptér forbindelsen og elevér UAC tilladelser"), + ("clipboard_wait_response_timeout_tip", "Tiden for at vente på en kopieringsforespørgsel udløb"), + ("Incoming connection", "Indgående forbindelse"), + ("Outgoing connection", "Udgående forbindelse"), + ("Exit", "Afslut"), + ("Open", "Åben"), + ("logout_tip", "Er du sikker på at du vil logge af?"), + ("Service", "Tjeneste"), + ("Start", "Start"), + ("Stop", "Stop"), + ("exceed_max_devices", "Du har nået det maksimale antal håndtérbare enheder."), + ("Sync with recent sessions", "Synkronisér med tidligere sessioner"), + ("Sort tags", "Sortér nøgleord"), + ("Open connection in new tab", "Åbn forbindelse i en ny fane"), + ("Move tab to new window", "Flyt fane i et nyt vindue"), + ("Can not be empty", "Kan ikke være tom"), + ("Already exists", "Findes allerede"), + ("Change Password", "Skift adgangskode"), + ("Refresh Password", "Genopfrisk adgangskode"), + ("ID", "ID"), + ("Grid View", "Gittervisning"), + ("List View", "Listevisning"), + ("Select", "Vælg"), + ("Toggle Tags", "Slå nøgleord til/fra"), + ("pull_ab_failed_tip", "Opdatering af adressebog mislykkedes"), + ("push_ab_failed_tip", "Synkronisering af adressebog til serveren mislykkedes"), + ("synced_peer_readded_tip", "Enhederne, som var til stede i de seneste sessioner, vil blive synkroniseret tilbage til adressebogen."), + ("Change Color", "Skift farve"), + ("Primary Color", "Primær farve"), + ("HSV Color", "HSV farve"), + ("Installation Successful!", "Installation fuldført!"), + ("Installation failed!", "Installation mislykkedes!"), + ("Reverse mouse wheel", "Invertér musehjul"), + ("{} sessions", "{} sessioner"), + ("scam_title", "ADVARSEL: Du kan blive SVINDLET!"), + ("scam_text1", "Hvis du taler telefon med en person du IKKE kender, og IKKE stoler på, som har bedt dig om at bruge RustDesk til at forbinde til din PC, stop med det samme, og læg på omgående."), + ("scam_text2", "Det er højest sandsynligvis en svinder som forsøger at stjæle dine penge eller andre personlige oplysninger."), + ("Don't show again", "Vis ikke igen"), + ("I Agree", "Jeg accepterer"), + ("Decline", "Afvis"), + ("Timeout in minutes", "Udløbstid i minutter"), + ("auto_disconnect_option_tip", "Luk automatisk indkommende sessioner ved inaktivitet"), + ("Connection failed due to inactivity", "Forbindelsen blev afbrudt grundet inaktivitet"), + ("Check for software update on startup", "Søg efter opdateringer ved opstart"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Opgradér venligst RustDesk Server Pro til version {} eller nyere!"), + ("pull_group_failed_tip", "Genindlæsning af gruppe mislykkedes"), + ("Filter by intersection", "Filtrér efter intersection"), + ("Remove wallpaper during incoming sessions", "Skjul baggrundsskærm ved indgående forbindelser"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Skærmen er slukket, skift til den første skærm."), + ("No displays", "Ingen skærme"), + ("Open in new window", "Åbn i et nyt vindue"), + ("Show displays as individual windows", "Vis skærme som selvstændige vinduer"), + ("Use all my displays for the remote session", "Brug alle mine skærme til fjernforbindelsen"), + ("selinux_tip", "SELinux er aktiveret på din enhed, som kan forhindre RustDesk i at køre normalt."), + ("Change view", "Skift visning"), + ("Big tiles", "Store fliser"), + ("Small tiles", "Små fliser"), + ("List", "Liste"), + ("Virtual display", "Virtuel skærm"), + ("Plug out all", "Frakobl alt"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Aktivér blokering af brugerstyring"), + ("id_input_tip", "Du kan indtaste ét ID, en direkte IP adresse, eller et domæne med en port (:).\nHvis du ønsker at forbinde til en enhed på en anden server, tilføj da server adressen (@?key=), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker adgang til en enhed på en offentlig server, indtast venligst \"@offentlig server\", nøglen er ikke nødvendig for offentlige servere.\n\nHvis du gerne vil tvinge brugen af en relay-forbindelse på den første forbindelse, tilføj \"/r\" efter ID'et, fx, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Tilstand 1"), + ("privacy_mode_impl_virtual_display_tip", "Tilstand 2"), + ("Enter privacy mode", "Start privatlivstilstand"), + ("Exit privacy mode", "Afslut privatlivstilstand"), + ("idd_not_support_under_win10_2004_tip", "Indirekte grafik drivere er ikke understøttet. Windows 10 version 2004 eller nyere er påkrævet."), + ("input_source_1_tip", "Input kilde 1"), + ("input_source_2_tip", "Input kilde 2"), + ("Swap control-command key", "Byt rundt på Control & Command tasterne"), + ("swap-left-right-mouse", "Byt rundt på venstre og højre musetaster"), + ("2FA code", "To-faktor kode"), + ("More", "Mere"), + ("enable-2fa-title", "Tænd for to-faktor godkendelse"), + ("enable-2fa-desc", "Åbn din godkendelsesapp nu. Du kan bruge en godkendelsesapp så som Authy, Microsoft eller Google Authenticator på din telefon eller din PC.\n\nScan QR koden med din app og indtast koden som din app fremviser, for at aktivere for to-faktor godkendelse."), + ("wrong-2fa-code", "Kan ikke verificere koden. Forsikr at koden og tidsindstillingerne på enheden er korrekte"), + ("enter-2fa-title", "To-faktor godkendelse"), + ("Email verification code must be 6 characters.", "E-mail bekræftelseskode skal være mindst 6 tegn"), + ("2FA code must be 6 digits.", "To-faktor kode skal være mindst 6 cifre"), + ("Multiple Windows sessions found", "Flere Windows sessioner fundet"), + ("Please select the session you want to connect to", "Vælg venligst sessionen du ønsker at forbinde til"), + ("powered_by_me", "Drives af RustDesk"), + ("outgoing_only_desk_tip", "Dette er en brugertilpasset udgave.\nDu kan forbinde til andre enheder, men andre enheder kan ikke forbinde til din enhed."), + ("preset_password_warning", "Denne brugertilpassede udgave har en forudbestemt adgangskode. Alle der kender til denne adgangskode, kan få fuld adgang til din enhed. Hvis du ikke forventede dette, bør du afinstallere denne udgave af RustDesk med det samme."), + ("Security Alert", "Sikkerhedsalarm"), + ("My address book", "Min adressebog"), + ("Personal", "Personlig"), + ("Owner", "Ejer"), + ("Set shared password", "Sæt delt adgangskode"), + ("Exist in", "Findes i"), + ("Read-only", "Skrivebeskyttet"), + ("Read/Write", "Læse/Skrive"), + ("Full Control", "Fuld kontrol"), + ("share_warning_tip", "Felterne for oven er delt og synlige for andre."), + ("Everyone", "Alle"), + ("ab_web_console_tip", "Mere på web konsollen"), + ("allow-only-conn-window-open-tip", "Tillad kun fjernforbindelser hvis RustDesk vinduet er synligt"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ingen fysiske skærme, ingen nødvendighed for at bruge privatlivstilstanden."), + ("Follow remote cursor", "Følg musemarkør på fjernforbindelse"), + ("Follow remote window focus", "Følg vinduefokus på fjernforbindelse"), + ("default_proxy_tip", "Protokollen og porten som anvendes som standard er Socks5 og 1080"), + ("no_audio_input_device_tip", "Ingen lydinput enhed fundet"), + ("Incoming", "Indgående"), + ("Outgoing", "Udgående"), + ("Clear Wayland screen selection", "Ryd Wayland skærmvalg"), + ("clear_Wayland_screen_selection_tip", "Efter at fravælge den valgte skærm, kan du genvælge skærmen som skal deles."), + ("confirm_clear_Wayland_screen_selection_tip", "Er du sikker på at du vil fjerne Wayland skærmvalget?"), + ("android_new_voice_call_tip", "Du har modtaget en ny stemmeopkaldsforespørgsel. Hvis du accepterer, vil lyden skifte til stemmekommunikation."), + ("texture_render_tip", "Brug tekstur-rendering for at gøre billedkvaliteten blødere. Du kan også prøve at deaktivere denne funktion, hvis du oplever problemer."), + ("Use texture rendering", "Anvend tekstur-rendering"), + ("Floating window", "Svævende vindue"), + ("floating_window_tip", "Det hjælper på at RustDesk baggrundstjenesten kører"), + ("Keep screen on", "Hold skærmen tændt"), + ("Never", "Aldrig"), + ("During controlled", "Imens under kontrol"), + ("During service is on", "Imens tjenesten kører"), + ("Capture screen using DirectX", "Optag skærm med DirectX"), + ("Back", "Tilbage"), + ("Apps", "Apps"), + ("Volume up", "Skru op for lyd"), + ("Volume down", "Skru ned for lyd"), + ("Power", "Tænd/Sluk"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Hvis du aktiverer denne funktion, kan du modtage to-faktor godkendelseskoden fra din robot. Den kan også fungere som en notifikation for forbindelsesanmodninger."), + ("enable-bot-desc", "1. Åbn en chat med @BotFather.\n2. Send kommandoen \"/newbot\". Du vil modtage en nøgle efter at have gennemført dette trin.\n3. Start en chat med din nyoprettede bot. Send en besked som begynder med skråstreg \"/\", som fx \"/hello\", for at aktivere den.\n"), + ("cancel-2fa-confirm-tip", "Er du sikker på at du vil afbryde to-faktor godkendelse?"), + ("cancel-bot-confirm-tip", "Er du sikker på at du vil afbryde Telegram robotten?"), + ("About RustDesk", "Om RustDesk"), + ("Send clipboard keystrokes", "Send udklipsholder tastetryk"), + ("network_error_tip", "Tjek venligst din internetforbindelse, og forsøg igen."), + ("Unlock with PIN", "Lås op med PIN"), + ("Requires at least {} characters", "Kræver mindst {} tegn"), + ("Wrong PIN", "Forkert PIN"), + ("Set PIN", "Sæt PIN"), + ("Enable trusted devices", "Aktivér troværdige enheder"), + ("Manage trusted devices", "Administrér troværdige enheder"), + ("Platform", "Platform"), + ("Days remaining", "Dage tilbage"), + ("enable-trusted-devices-tip", "Spring to-faktor godkendelse over på troværdige enheder"), + ("Parent directory", "mappe"), + ("Resume", "Fortsæt"), + ("Invalid file name", "Ugyldigt filnavn"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e8e61543a48..a732213712a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), ("Lock after session end", "Nach Sitzungsende sperren"), - ("Insert", "Einfügen"), + ("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), + ("Automatically record outgoing sessions", ""), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), @@ -548,7 +549,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("Filter by intersection", "Nach Schnittmenge filtern"), ("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"), - ("Test", "Test"), + ("Test", "Testen"), ("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."), ("No displays", "Keine Bildschirme"), ("Open in new window", "In einem neuen Fenster öffnen"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Alle ausschalten"), ("True color (4:4:4)", "True Color (4:4:4)"), ("Enable blocking user input", "Blockieren von Benutzereingaben aktivieren"), - ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen möchten, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), + ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "Modus 1"), ("privacy_mode_impl_virtual_display_tip", "Modus 2"), ("Enter privacy mode", "Datenschutzmodus aktivieren"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Übergeordnetes Verzeichnis"), ("Resume", "Fortsetzen"), ("Invalid file name", "Ungültiger Dateiname"), + ("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."), + ("Authentication Required", "Authentifizierung erforderlich"), + ("Authenticate", "Authentifizieren"), + ("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."), + ("Download", "Herunterladen"), + ("Upload folder", "Ordner hochladen"), + ("Upload files", "Dateien hochladen"), + ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index b3ce7dcaf80..e6df3bc3d5b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", "Εισαγωγή"), + ("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"), ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Εγγραφή"), ("Directory", "Φάκελος εγγραφών"), ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), + ("Automatically record outgoing sessions", ""), ("Change", "Αλλαγή"), ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 917422d0dc4..7ed83a8fe4f 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -234,5 +234,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", ""), ("network_error_tip", "Please check your network connection, then click retry."), ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 144bf7bc3c4..876c901a481 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -6,11 +6,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("desk_tip", "Via aparato povas esti alirita kun tiu identigilo kaj pasvorto"), ("Password", "Pasvorto"), ("Ready", "Preta"), - ("Established", ""), + ("Established", "Establis"), ("connecting_status", "Konektante al la reto RustDesk..."), ("Enable service", "Ebligi servon"), ("Start service", "Starti servon"), - ("Service is running", ""), + ("Service is running", "La servo funkcias"), ("Service is not running", "La servo ne funkcias"), ("not_ready_status", "Ne preta, bonvolu kontroli la retkonekto"), ("Control Remote Desktop", "Kontroli foran aparaton"), @@ -29,33 +29,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP tunneling", "Ebligi tunelado TCP"), ("IP Whitelisting", "Listo de IP akceptataj"), ("ID/Relay Server", "Identigila/Relajsa servilo"), - ("Import server config", "Enporti servilan agordon"), - ("Export Server Config", ""), + ("Import server config", "Importi servilan agordon"), + ("Export Server Config", "Eksporti servilan agordon"), ("Import server configuration successfully", "Importi servilan agordon sukcese"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Eksporti servilan agordon sukcese"), ("Invalid server configuration", "Nevalida servila agordo"), ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Via nova identigilo"), + ("length %min% to %max%", "longeco %min% al %max%"), + ("starts with a letter", "komencas kun letero"), + ("allowed characters", "permesitaj signoj"), ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Website", "Retejo"), ("About", "Pri"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Farita kun koro en ĉi tiu ĥaosa mondo!"), + ("Privacy Statement", "Deklaro Pri Privateco"), ("Mute", "Muta"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), - ("Audio Input", "Aŭdia enigo"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive bitrate", ""), + ("Build Date", "konstruada dato"), + ("Version", "Versio"), + ("Home", "Hejmo"), + ("Audio Input", "Aŭdia Enigo"), + ("Enhancements", "Plibonigoj"), + ("Hardware Codec", "Aparataro Kodeko"), + ("Adaptive bitrate", "Adapta bitrapido"), ("ID Server", "Servilo de identigiloj"), - ("Relay Server", "Relajsa servilo"), + ("Relay Server", "Relajsa Servilo"), ("API Server", "Servilo de API"), ("invalid_http", "Devas komenci kun http:// aŭ https://"), ("Invalid IP", "IP nevalida"), @@ -83,35 +83,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Successful", "Sukceso"), ("Connected, waiting for image...", "Konektita, atendante bildon..."), ("Name", "Nomo"), - ("Type", ""), + ("Type", "Tipo"), ("Modified", "Modifita"), ("Size", "Grandeco"), ("Show Hidden Files", "Montri kaŝitajn dosierojn"), ("Receive", "Akcepti"), ("Send", "Sendi"), - ("Refresh File", ""), - ("Local", ""), - ("Remote", ""), + ("Refresh File", "Aktualigu Dosieron"), + ("Local", "Loka"), + ("Remote", "Fora"), ("Remote Computer", "Fora komputilo"), ("Local Computer", "Loka komputilo"), ("Confirm Delete", "Konfermi la forigo"), - ("Delete", ""), - ("Properties", ""), - ("Multi Select", ""), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", ""), - ("Not an empty directory", ""), - ("Are you sure you want to delete this file?", "Ĉu vi vere volas forigi tiun dosieron?"), - ("Are you sure you want to delete this empty directory?", ""), - ("Are you sure you want to delete the file of this directory?", ""), + ("Delete", "Forigi"), + ("Properties", "Propraĵoj"), + ("Multi Select", "Pluropa Elekto"), + ("Select All", "Elektu Ĉiujn"), + ("Unselect All", "Malelektu Ĉiujn"), + ("Empty Directory", "Malplena Dosierujo"), + ("Not an empty directory", "Ne Malplena Dosierujo"), + ("Are you sure you want to delete this file?", "Ĉu vi certas, ke vi volas forigi ĉi tiun dosieron?"), + ("Are you sure you want to delete this empty directory?", "Ĉu vi certas, ke vi volas forigi ĉi tiun malplenan dosierujon?"), + ("Are you sure you want to delete the file of this directory?", "Ĉu vi certa. ke vi volas forigi la dosieron de ĉi tiu dosierujo"), ("Do this for all conflicts", "Same por ĉiuj konfliktoj"), - ("This is irreversible!", ""), + ("This is irreversible!", "Ĉi tio estas neinversigebla!"), ("Deleting", "Forigado"), ("files", "dosiero"), ("Waiting", "Atendante..."), ("Finished", "Finita"), - ("Speed", ""), + ("Speed", "Rapideco"), ("Custom Image Quality", "Agordi bildan kvaliton"), ("Privacy mode", "Modo privata"), ("Block user input", "Bloki uzanta enigo"), @@ -127,10 +127,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Optimigi reakcia tempo"), ("Custom", ""), ("Show remote cursor", "Montri foran kursoron"), - ("Show quality monitor", ""), + ("Show quality monitor", "Montri kvalito monitoron"), ("Disable clipboard", "Malebligi poŝon"), ("Lock after session end", "Ŝlosi foran komputilon post malkonektado"), - ("Insert", "Enmeti"), + ("Insert Ctrl + Alt + Del", "Enmeti Ctrl + Alt + Del"), ("Insert Lock", "Ŝlosi foran komputilon"), ("Refresh", "Refreŝigi ekranon"), ("ID does not exist", "La identigilo ne ekzistas"), @@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Ago"), ("Add", "Aldoni"), ("Local Port", "Loka pordo"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Loka Adreso"), + ("Change Local Port", "Ŝanĝi Loka Pordo"), ("setup_server_tip", "Se vi bezonas pli rapida konekcio, vi povas krei vian propran servilon"), ("Too short, at least 6 characters.", "Tro mallonga, almenaŭ 6 signoj."), ("The confirmation is not identical.", "Ambaŭ enigoj ne kongruas"), @@ -203,23 +203,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Restarto deviga"), ("Unsupported display server", "La aktuala bilda servilo ne estas subtenita"), ("x11 expected", "Bonvolu uzi x11"), - ("Port", ""), + ("Port", "Pordo"), ("Settings", "Agordoj"), ("Username", " Uzanta nomo"), ("Invalid port", "Pordo nevalida"), ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Connect via relay", ""), + ("Connect via relay", "Konekti per relajso"), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), - ("Login", "Konekti"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), - ("Logout", "Malkonekti"), + ("Login", "Ensaluti"), + ("Verify", "Kontrolis"), + ("Remember me", "Memori min"), + ("Trust this device", "Fidu ĉi tiun aparaton"), + ("Verification code", "Konfirmkodo"), + ("verification_tip", "Konfirmkodo estis sendita al la registrita retpoŝta adreso, enigu la konfirmkodon por daŭrigi ensaluti."), + ("Logout", "Elsaluti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), @@ -241,86 +241,86 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Socks5 prokura servilo"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"), ("Discovered", "Malkovritaj"), - ("install_daemon_tip", ""), + ("install_daemon_tip", "Por komenci ĉe ekŝargo, oni devas instali sisteman servon."), ("Remote ID", "Fora identigilo"), ("Paste", "Alglui"), - ("Paste here?", ""), + ("Paste here?", "Alglui ĉi tie?"), ("Are you sure to close the connection?", "Ĉu vi vere volas fermi la konekton?"), ("Download new version", "Elŝuti la novan version"), ("Touch mode", "Tuŝa modo"), - ("Mouse mode", ""), - ("One-Finger Tap", ""), - ("Left Mouse", ""), - ("One-Long Tap", ""), - ("Two-Finger Tap", ""), - ("Right Mouse", ""), - ("One-Finger Move", ""), - ("Double Tap & Move", ""), - ("Mouse Drag", ""), - ("Three-Finger vertically", ""), - ("Mouse Wheel", ""), - ("Two-Finger Move", ""), - ("Canvas Move", ""), - ("Pinch to Zoom", ""), - ("Canvas Zoom", ""), + ("Mouse mode", "musa modo"), + ("One-Finger Tap", "Unufingra Frapeto"), + ("Left Mouse", "Maldekstra Muso"), + ("One-Long Tap", "Unulonga Frapeto"), + ("Two-Finger Tap", "Dufingra Frapeto"), + ("Right Mouse", "Deskra Muso"), + ("One-Finger Move", "Unufingra Movo"), + ("Double Tap & Move", "Duobla Frapeto & Movo"), + ("Mouse Drag", "Muso Trenadi"), + ("Three-Finger vertically", "Tri Figroj Vertikale"), + ("Mouse Wheel", "Musa Rado"), + ("Two-Finger Move", "Dufingra Movo"), + ("Canvas Move", "Kanvasa Movo"), + ("Pinch to Zoom", "Pinĉi al Zomo"), + ("Canvas Zoom", "Kanvasa Zomo"), ("Reset canvas", "Restarigi kanvaso"), ("No permission of file transfer", "Neniu permeso de dosiertransigo"), ("Note", "Notu"), - ("Connection", ""), - ("Share Screen", ""), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), - ("Screen Capture", ""), - ("Input Control", ""), - ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), - ("Do you accept?", ""), - ("Open System Setting", ""), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", ""), - ("android_input_permission_tip2", ""), - ("android_new_connection_tip", ""), - ("android_service_will_start_tip", ""), - ("android_stop_service_tip", ""), - ("android_version_audio_tip", ""), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), - ("Account", ""), - ("Overwrite", ""), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), - ("Someone turns on privacy mode, exit", ""), - ("Unsupported", ""), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), - ("Language", ""), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), + ("Connection", "Konekto"), + ("Share Screen", "Kunhavigi Ekranon"), + ("Chat", "Babilo"), + ("Total", "Sumo"), + ("items", "eroj"), + ("Selected", "Elektita"), + ("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"), + ("android_input_permission_tip1", "Por ke fora aparato regu vian Android-aparaton per muso aŭ tuŝo, vi devas permesi al RustDesk uzi la servon \"Alirebleco\"."), + ("android_input_permission_tip2", "Bonvolu iri al la sekva paĝo de sistemaj agordoj, trovi kaj eniri [Instatajn Servojn], ŝalti la servon [RustDesk Enigo]."), + ("android_new_connection_tip", "Nova kontrolpeto estis ricevita, kiu volas kontroli vian nunan aparaton."), + ("android_service_will_start_tip", "Ŝalti \"Ekrankapto\" aŭtomate startos la servon, permesante al aliaj aparatoj peti konekton al via aparato."), + ("android_stop_service_tip", "Fermante la servon aŭtomate fermos ĉiujn establitajn konektojn."), + ("android_version_audio_tip", "La nuna versio da Android ne subtenas sonkapton, bonvolu ĝisdatigi al Android 10 aŭ pli alta."), + ("android_start_service_tip", "Frapu [Komenci servo] aŭ ebligu la permeson de [Ekrankapto] por komenci la servon de kundivido de ekrano."), + ("android_permission_may_not_change_tip", "Permesoj por establitaj konektoj neble estas ŝanĝitaj tuj ĝis rekonektitaj."), + ("Account", "Konto"), + ("Overwrite", "anstataŭigi"), + ("This file exists, skip or overwrite this file?", "Ĉi tiu dosiero ekzistas, ĉu preterlasi aŭ anstataŭi ĉi tiun dosieron?"), + ("Quit", "Forlasi"), + ("Help", "Helpi"), + ("Failed", "Malsukcesa"), + ("Succeeded", "Sukcesa"), + ("Someone turns on privacy mode, exit", "Iu ŝaltas modon privata, Eliro"), + ("Unsupported", "Nesubtenata"), + ("Peer denied", "Samulo rifuzita"), + ("Please install plugins", "Bonvolu instali kromprogramojn"), + ("Peer exit", "Samulo eliras"), + ("Failed to turn off", "Malsukcesis malŝalti"), + ("Turned off", "Malŝaltita"), + ("Language", "Lingvo"), + ("Keep RustDesk background service", "Tenu RustDesk fonan servon"), + ("Ignore Battery Optimizations", "Ignoru Bateria Optimumigojn"), + ("android_open_battery_optimizations_tip", "Se vi volas malŝalti ĉi tiun funkcion, bonvolu iri al la sekva paĝo de agordoj de la aplikaĵo de RustDesk, trovi kaj eniri [Baterio], Malmarku [Senrestrikta]"), + ("Start on boot", "Komencu ĉe ekfunkciigo"), + ("Start the screen sharing service on boot, requires special permissions", "Komencu la servon de kundivido de ekrano ĉe lanĉo, postulas specialajn permesojn"), + ("Connection not allowed", "Konekto ne rajtas"), ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Enable remote restart", ""), - ("Restart remote device", ""), - ("Are you sure you want to restart", ""), - ("Restarting remote device", ""), - ("remote_restarting_tip", ""), - ("Copied", ""), + ("Map mode", "Mapa modo"), + ("Translate mode", "Traduki modo"), + ("Use permanent password", "Uzu permanenta pasvorto"), + ("Use both passwords", "Uzu ambaŭ pasvorto"), + ("Set permanent password", "Starigi permanenta pasvorto"), + ("Enable remote restart", "Permesi fora restartas"), + ("Restart remote device", "Restartu fora aparato"), + ("Are you sure you want to restart", "Ĉu vi certas, ke vi volas restarti"), + ("Restarting remote device", "Restartas fora aparato"), + ("remote_restarting_tip", "Fora aparato restartiĝas, bonvolu fermi ĉi tiun mesaĝkeston kaj rekonekti kun permanenta pasvorto post iom da tempo"), + ("Copied", "Kopiita"), ("Exit Fullscreen", "Eliru Plenekranon"), ("Fullscreen", "Plenekrane"), ("Mobile Actions", "Poŝtelefonaj Agoj"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporcio"), ("Image Quality", "Bilda Kvalito"), ("Scroll Style", "Ruluma Stilo"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Montri Ilobreton"), + ("Hide Toolbar", "Kaŝi Ilobreton"), ("Direct Connection", "Rekta Konekto"), ("Relay Connection", "Relajsa Konekto"), ("Secure Connection", "Sekura Konekto"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 579d770bd50..ce77f620c71 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Mostrar calidad del monitor"), ("Disable clipboard", "Deshabilitar portapapeles"), ("Lock after session end", "Bloquear después del final de la sesión"), - ("Insert", "Insertar"), + ("Insert Ctrl + Alt + Del", "Insertar Ctrl + Alt + Del"), ("Insert Lock", "Insertar bloqueo"), ("Refresh", "Actualizar"), ("ID does not exist", "La ID no existe"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Grabando"), ("Directory", "Directorio"), ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), + ("Automatically record outgoing sessions", ""), ("Change", "Cambiar"), ("Start session recording", "Comenzar grabación de sesión"), ("Stop session recording", "Detener grabación de sesión"), @@ -394,7 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "Aceptar sesiones a través de clic"), ("Accept sessions via both", "Aceptar sesiones a través de ambos"), ("Please wait for the remote side to accept your session request...", "Por favor, espere a que el lado remoto acepte su solicitud de sesión"), - ("One-time Password", "Constaseña de un solo uso"), + ("One-time Password", "Contraseña de un solo uso"), ("Use one-time password", "Usar contraseña de un solo uso"), ("One-time password length", "Longitud de la contraseña de un solo uso"), ("Request access to your device", "Solicitud de acceso a su dispositivo"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Directorio superior"), ("Resume", "Continuar"), ("Invalid file name", "Nombre de archivo no válido"), + ("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."), + ("Authentication Required", "Se requiere autenticación"), + ("Authenticate", "Autenticar"), + ("web_id_input_tip", "Puedes introducir una ID en el mismo servidor, el cliente web no soporta acceso vía IP.\nSi quieres acceder a un dispositivo en otro servidor, por favor, agrega la dirección del servidor (@?clave=), por ejemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi quieres accedder a un dispositivo en un servidor público, por favor, introduce \"@public\", la clave no es necesaria para el servidor público."), + ("Download", "Descarga"), + ("Upload folder", "Subir carpeta"), + ("Upload files", "Subir archivos"), + ("Clipboard is synchronized", "Portapapeles sincronizado"), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index cc3f3afc392..21de56c9e69 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "Sisesta lukk"), ("Refresh", ""), ("ID does not exist", ""), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 412b4e74060..ac958d79c67 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Erakutsi kalitate monitorea"), ("Disable clipboard", "Desgaitu arbela"), ("Lock after session end", "Blokeatu sesioa amaitu ostean"), - ("Insert", "Sartu"), + ("Insert Ctrl + Alt + Del", "Sartu Ctrl + Alt + Del"), ("Insert Lock", "Sarrera-blokeoa"), ("Refresh", "Freskatu"), ("ID does not exist", "IDa ez da existitzen"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Grabatzen"), ("Directory", "Direktorioa"), ("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"), + ("Automatically record outgoing sessions", ""), ("Change", "Aldatu"), ("Start session recording", "Hasi saioaren grabaketa"), ("Stop session recording", "Gelditu saioaren grabaketa"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b6949aa5043..ff43815739a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "نمایش کیفیت مانیتور"), ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), - ("Insert", "افزودن"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del افزودن"), ("Insert Lock", "قفل کردن سیستم"), ("Refresh", "تازه سازی"), ("ID does not exist", "شناسه وجود ندارد"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "در حال ضبط"), ("Directory", "مسیر"), ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), + ("Automatically record outgoing sessions", ""), ("Change", "تغییر"), ("Start session recording", "شروع ضبط جلسه"), ("Stop session recording", "توقف ضبط جلسه"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 31d78640888..85b1354c318 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"), - ("Insert", "Envoyer"), + ("Insert Ctrl + Alt + Del", "Envoyer Ctrl + Alt + Del"), ("Insert Lock", "Verrouiller l'appareil distant"), ("Refresh", "Rafraîchir l'écran"), ("ID does not exist", "L'ID n'existe pas"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Enregistrement"), ("Directory", "Répertoire"), ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifier"), ("Start session recording", "Commencer l'enregistrement"), ("Stop session recording", "Stopper l'enregistrement"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 07d9aa977d1..b63d4212246 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", "הוסף נעילה"), ("Refresh", ""), ("ID does not exist", ""), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 1dca1c7e0c0..4a3136c833f 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži kvalitetu monitora"), ("Disable clipboard", "Zabrani međuspremnik"), ("Lock after session end", "Zaključaj po završetku sesije"), - ("Insert", "Umetni"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), ("Insert Lock", "Zaključaj umetanje"), ("Refresh", "Osvježi"), ("ID does not exist", "ID ne postoji"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snimanje"), ("Directory", "Mapa"), ("Automatically record incoming sessions", "Automatski snimi dolazne sesije"), + ("Automatically record outgoing sessions", ""), ("Change", "Promijeni"), ("Start session recording", "Započni snimanje sesije"), ("Stop session recording", "Zaustavi snimanje sesije"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index be089347aca..f1f8ac1ae04 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", "Közös vágólap kikapcsolása"), ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del"), ("Insert Lock", "Távoli fiók zárolása"), ("Refresh", "Frissítés"), ("ID does not exist", "Az azonosító nem létezik"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Felvétel"), ("Directory", "Könyvtár"), ("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"), + ("Automatically record outgoing sessions", ""), ("Change", "Változtatás"), ("Start session recording", "Munkamenet rögzítés indítása"), ("Stop session recording", "Munkamenet rögzítés leállítása"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 42c36b5c744..066f2980cc1 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Desktop Anda"), - ("desk_tip", "Desktop Anda dapat diakses dengan ID dan kata sandi ini."), + ("Your Desktop", "Layar Utama"), + ("desk_tip", "Layar kamu dapat diakses dengan ID dan kata sandi ini."), ("Password", "Kata sandi"), ("Ready", "Sudah siap"), ("Established", "Didirikan"), @@ -12,17 +12,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start service", "Mulai Layanan"), ("Service is running", "Layanan berjalan"), ("Service is not running", "Layanan tidak berjalan"), - ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"), - ("Control Remote Desktop", "Kontrol Remote Desktop"), - ("Transfer file", "File Transfer"), - ("Connect", "Hubungkan"), + ("not_ready_status", "Belum siap digunakan. Silakan periksa koneksi"), + ("Control Remote Desktop", "Kontrol PC dari jarak jauh"), + ("Transfer file", "Transfer File"), + ("Connect", "Sambungkan"), ("Recent sessions", "Sesi Terkini"), ("Address book", "Buku Alamat"), ("Confirmation", "Konfirmasi"), ("TCP tunneling", "Tunneling TCP"), ("Remove", "Hapus"), ("Refresh random password", "Perbarui kata sandi acak"), - ("Set your own password", "Tetapkan kata sandi Anda"), + ("Set your own password", "Tetapkan kata sandi"), ("Enable keyboard/mouse", "Aktifkan Keyboard/Mouse"), ("Enable clipboard", "Aktifkan Papan Klip"), ("Enable file transfer", "Aktifkan Transfer file"), @@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), - ("Your new ID", "ID baru anda"), + ("Your new ID", "ID baru"), ("length %min% to %max%", "panjang %min% s/d %max%"), ("starts with a letter", "Dimulai dengan huruf"), ("allowed characters", "Karakter yang dapat digunakan"), @@ -69,10 +69,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Retry", "Coba lagi"), ("OK", "Oke"), ("Password Required", "Kata sandi tidak boleh kosong"), - ("Please enter your password", "Silahkan masukkan kata sandi anda"), + ("Please enter your password", "Silahkan masukkan kata sandi"), ("Remember password", "Ingat kata sandi"), ("Wrong Password", "Kata sandi Salah"), - ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"), + ("Do you want to enter again?", "Apakah kamu ingin mencoba lagi?"), ("Connection Error", "Kesalahan koneksi"), ("Error", "Kesalahan"), ("Reset by the peer", "Direset oleh rekan"), @@ -102,9 +102,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "Batalkan Pilihan Semua"), ("Empty Directory", "Folder Kosong"), ("Not an empty directory", "Folder tidak kosong"), - ("Are you sure you want to delete this file?", "Apakah anda yakin untuk menghapus file ini?"), - ("Are you sure you want to delete this empty directory?", "Apakah anda yakin untuk menghapus folder ini?"), - ("Are you sure you want to delete the file of this directory?", "Apakah anda yakin untuk menghapus file dan folder ini?"), + ("Are you sure you want to delete this file?", "Apakah kamu yakin untuk menghapus file ini?"), + ("Are you sure you want to delete this empty directory?", "Apakah yakin yakin untuk menghapus folder ini?"), + ("Are you sure you want to delete the file of this directory?", "Apakah yakin yakin untuk menghapus file dan folder ini?"), ("Do this for all conflicts", "Lakukan untuk semua konflik"), ("This is irreversible!", "Ini tidak dapat diubah!"), ("Deleting", "Menghapus"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Tampilkan kualitas monitor"), ("Disable clipboard", "Matikan papan klip"), ("Lock after session end", "Kunci setelah sesi berakhir"), - ("Insert", "Menyisipkan"), + ("Insert Ctrl + Alt + Del", "Menyisipkan Ctrl + Alt + Del"), ("Insert Lock", "Masukkan Kunci"), ("Refresh", "Segarkan"), ("ID does not exist", "ID tidak ada"), @@ -150,20 +150,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Klik untuk unduh"), ("Click to update", "Klik untuk memperbarui"), ("Configure", "Konfigurasi"), - ("config_acc", "Untuk mengontrol Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Aksesibilitas\" RustDesk."), - ("config_screen", "Untuk mengakses Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Perekaman Layar\" RustDesk."), + ("config_acc", "Agar bisa mengontrol Desktopmu dari jarak jauh, Kamu harus memberikan izin \"Aksesibilitas\" untuk RustDesk."), + ("config_screen", "Agar bisa mengakses Desktopmu dari jarak jauh, kamu harus memberikan izin \"Perekaman Layar\" untuk RustDesk."), ("Installing ...", "Menginstall"), ("Install", "Instal"), ("Installation", "Instalasi"), ("Installation Path", "Direktori Instalasi"), ("Create start menu shortcuts", "Buat pintasan start menu"), ("Create desktop icon", "Buat icon desktop"), - ("agreement_tip", "Dengan memulai instalasi, Anda menerima perjanjian lisensi."), + ("agreement_tip", "Dengan memulai proses instalasi, Kamu menerima perjanjian lisensi."), ("Accept and Install", "Terima dan Install"), ("End-user license agreement", "Perjanjian lisensi pengguna"), ("Generating ...", "Memproses..."), - ("Your installation is lower version.", "Instalasi Anda adalah versi yang lebih rendah."), - ("not_close_tcp_tip", "Jangan tutup jendela ini saat menggunakan tunnel"), + ("Your installation is lower version.", "Kamu menggunakan versi instalasi yang lebih rendah."), + ("not_close_tcp_tip", "Pastikan jendela ini tetap terbuka saat menggunakan tunnel."), ("Listening ...", "Menghubungkan..."), ("Remote Host", "Host Remote"), ("Remote Port", "Port Remote"), @@ -172,24 +172,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Port Lokal"), ("Local Address", "Alamat lokal"), ("Change Local Port", "Ubah Port Lokal"), - ("setup_server_tip", "Untuk mendapatkan koneksi yang lebih baik, disarankan untuk menginstal di server anda sendiri"), + ("setup_server_tip", "Untuk koneksi yang lebih baik, silakan konfigurasi di server pribadi"), ("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."), ("The confirmation is not identical.", "Konfirmasi tidak identik."), ("Permissions", "Perizinan"), ("Accept", "Terima"), ("Dismiss", "Hentikan"), ("Disconnect", "Terputus"), - ("Enable file copy and paste", "Izinkan salin dan tempel file"), + ("Enable file copy and paste", "Izinkan copy dan paste"), ("Connected", "Terhubung"), ("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"), ("Relayed and encrypted connection", "Koneksi relay dan terenkripsi"), ("Direct and unencrypted connection", "Koneksi langsung dan tanpa enkripsi"), ("Relayed and unencrypted connection", "Koneksi relay dan tanpa enkripsi"), ("Enter Remote ID", "Masukkan ID Remote"), - ("Enter your password", "Masukkan kata sandi anda"), + ("Enter your password", "Masukkan kata sandi"), ("Logging in...", "Masuk..."), ("Enable RDP session sharing", "Aktifkan berbagi sesi RDP"), - ("Auto Login", "Login Otomatis (Hanya berlaku jika Anda mengatur \"Kunci setelah sesi berakhir\")"), + ("Auto Login", "Login Otomatis (Hanya berlaku jika sudah mengatur \"Kunci setelah sesi berakhir\")"), ("Enable direct IP access", "Aktifkan Akses IP Langsung"), ("Rename", "Ubah nama"), ("Space", "Spasi"), @@ -199,7 +199,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Silahkan masukkan nama folder"), ("Fix it", "Perbaiki"), ("Warning", "Peringatan"), - ("Login screen using Wayland is not supported", "Layar masuk menggunakan Wayland tidak didukung"), + ("Login screen using Wayland is not supported", "Login screen dengan Wayland tidak didukung"), ("Reboot required", "Diperlukan boot ulang"), ("Unsupported display server", "Server tampilan tidak didukung "), ("x11 expected", "Diperlukan x11"), @@ -241,11 +241,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Proksi Socks5"), ("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"), ("Discovered", "Telah ditemukan"), - ("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."), + ("install_daemon_tip", "Untuk dapat berjalan saat sistem menyala, kamu perlu menginstal layanan sistem (system service/daemon)."), ("Remote ID", "ID Remote"), ("Paste", "Tempel"), ("Paste here?", "Tempel disini?"), - ("Are you sure to close the connection?", "Apakah anda yakin akan menutup koneksi?"), + ("Are you sure to close the connection?", "Apakah kamu yakin akan menutup koneksi?"), ("Download new version", "Unduh versi baru"), ("Touch mode", "Mode Layar Sentuh"), ("Mouse mode", "Mode Mouse"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Perekaman"), ("Directory", "Direktori"), ("Automatically record incoming sessions", "Otomatis merekam sesi masuk"), + ("Automatically record outgoing sessions", ""), ("Change", "Ubah"), ("Start session recording", "Mulai sesi perekaman"), ("Stop session recording", "Hentikan sesi perekaman"), @@ -373,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Tulis pesan"), ("Prompt", ""), ("Please wait for confirmation of UAC...", "Harap tunggu konfirmasi UAC"), - ("elevated_foreground_window_tip", "Jendela remote desktop ini memerlukan hak akses khusus, jadi anda tidak bisa menggunakan mouse dan keyboard untuk sementara. Anda bisa meminta pihak pengguna yang diremote untuk menyembunyikan jendela ini atau klik tombol elevasi di jendela pengaturan koneksi. Untuk menghindari masalah ini, direkomendasikan untuk menginstall aplikasi secara permanen"), + ("elevated_foreground_window_tip", "Jendela yang sedang aktif di remote desktop memerlukan hak istimewa yang lebih tinggi untuk beroperasi, sehingga mouse dan keyboard tidak dapat digunakan sementara waktu. Kamu bisa meminta pengguna jarak jauh untuk meminimalkan jendela saat ini, atau klik tombol elevasi di jendela manajemen koneksi. Untuk menghindari masalah ini, disarankan untuk menginstal software di perangkat remote secara permanen."), ("Disconnected", "Terputus"), ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), @@ -394,9 +395,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "Izinkan sesi dengan klik"), ("Accept sessions via both", "Izinkan sesi dengan keduanya"), ("Please wait for the remote side to accept your session request...", "Harap tunggu pihak pengguna remote untuk menerima permintaan sesi..."), - ("One-time Password", "Kata sandi sekali pakai"), - ("Use one-time password", "Gunakan kata sandi sekali pakai"), - ("One-time password length", "Panjang kata sandi sekali pakai"), + ("One-time Password", "Kata sandi sementara"), + ("Use one-time password", "Gunakan kata sandi sementara"), + ("One-time password length", "Panjang kata sandi sementara"), ("Request access to your device", "Permintaan akses ke perangkat ini"), ("Hide connection management window", "Sembunyikan jendela pengaturan koneksi"), ("hide_cm_tip", "Izinkan untuk menyembunyikan hanya jika menerima sesi melalui kata sandi dan menggunakan kata sandi permanen"), @@ -569,80 +570,88 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter privacy mode", "Masuk mode privasi"), ("Exit privacy mode", "Keluar mode privasi"), ("idd_not_support_under_win10_2004_tip", "Driver grafis yang Anda gunakan tidak kompatibel dengan versi Windows Anda dan memerlukan Windows 10 versi 2004 atau yang lebih baru"), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), + ("input_source_1_tip", "Sumber input 1"), + ("input_source_2_tip", "Sumber input 2"), + ("Swap control-command key", "Menukar tombol control-command"), + ("swap-left-right-mouse", "Tukar fungsi tombol kiri dan kanan pada mouse"), + ("2FA code", "Kode 2FA"), + ("More", "Lainnya"), + ("enable-2fa-title", "Aktifkan autentikasi 2FA"), + ("enable-2fa-desc", "Silakan atur autentikator Anda sekarang. Anda dapat menggunakan aplikasi autentikator seperti Authy, Microsoft Authenticator, atau Google Authenticator di ponsel atau desktop Anda\n\nPindai kode QR dengan aplikasi Anda dan masukkan kode yang ditampilkan oleh aplikasi untuk mengaktifkan autentikasi 2FA."), + ("wrong-2fa-code", "Tidak dapat memverifikasi kode. Pastikan bahwa kode dan pengaturan waktu lokal sudah sesuai"), + ("enter-2fa-title", "Autentikasi dua faktor"), + ("Email verification code must be 6 characters.", "Kode verifikasi email harus terdiri dari 6 karakter."), + ("2FA code must be 6 digits.", "Kode 2FA harus terdiri dari 6 digit."), + ("Multiple Windows sessions found", "Terdapat beberapa sesi Windows"), + ("Please select the session you want to connect to", "Silakan pilih sesi yang ingin Anda sambungkan."), + ("powered_by_me", "Didukung oleh RustDesk"), + ("outgoing_only_desk_tip", "Ini adalah edisi yang sudah kustomisasi.\nAnda dapat terhubung ke perangkat lain, tetapi perangkat lain tidak dapat terhubung ke perangkat Anda."), + ("preset_password_warning", "Edisi yang dikustomisasi ini dilengkapi dengan kata sandi bawaan. Siapa pun yang mengetahui kata sandi ini dapat memperoleh kontrol penuh atas perangkat Anda. Jika Anda tidak mengharapkan ini, segera hapus pemasangan aplikasi tersebut."), + ("Security Alert", "Peringatan Keamanan"), + ("My address book", "Daftar Kontak"), + ("Personal", "Personal"), + ("Owner", "Pemilik"), + ("Set shared password", "Atus kata sandi kolaboratif"), + ("Exist in", "Ada di"), ("Read-only", ""), ("Read/Write", ""), ("Full Control", ""), - ("share_warning_tip", ""), + ("share_warning_tip", "Informasi di atas bersifat publik dan dapat dilihat oleh orang lain."), ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("ab_web_console_tip", "Detail Lain di Konsol Web"), + ("allow-only-conn-window-open-tip", "Koneksi hanya diperbolehkan jika jendela RustDesk sedang terbuka."), + ("no_need_privacy_mode_no_physical_displays_tip", "Karena tidak ada layar fisik, mode privasi tidak perlu diaktifkan."), + ("Follow remote cursor", "Ikuti kursor yang terhubung"), + ("Follow remote window focus", "Ikuti jendela remote yang sedang aktif"), + ("default_proxy_tip", "Pengaturan standar untuk protokol dan port adalah Socks5 dan 1080."), + ("no_audio_input_device_tip", "Perangkat input audio tidak terdeteksi."), ("Incoming", ""), ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), + ("Clear Wayland screen selection", "Kosongkan pilihan layar Wayland"), + ("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Kamu bisa memilih kembali layar untuk dibagi"), + ("confirm_clear_Wayland_screen_selection_tip", "Kamu yakin ingin membersihkan pemilihan layar Wayland?"), + ("android_new_voice_call_tip", "Kamu mendapatkan permintaan panggilan suara baru. Jika diterima, audio akan berubah menjadi komunikasi suara."), + ("texture_render_tip", "Aktifkan rendering tekstur untuk membuat tampilan gambar lebih mulus. Kamu dapat menonaktifkan opsi ini jika terjadi masalah saat merender."), + ("Use texture rendering", "Aktifkan rendering tekstur"), ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), + ("floating_window_tip", "Untuk menjaga layanan/service RustDesk agar tetap aktif"), + ("Keep screen on", "Biarkan layar tetap menyala"), + ("Never", "Tidak pernah"), + ("During controlled", "Dalam proses pengendalian"), ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Capture screen using DirectX", "Rekam layar dengan DirectX"), + ("Back", "Kembali"), + ("Apps", "App"), + ("Volume up", "Naikkan volume"), + ("Volume down", "Turunkan volume"), ("Power", ""), ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("enable-bot-tip", "Jika fitur ini diaktifkan, Kamu dapat menerima kode 2FA dari bot, serta mendapatkan notifikasi tentang koneksi."), + ("enable-bot-desc", "1. Buka chat dengan @BotFather.\n2. Kirim perintah \"/newbot\". Setelah menyelesaikan langkah ini, Kamu akan mendapatkan token\n3. Mulai percakapan dengan bot yang baru dibuat. Kirim pesan yang dimulai dengan garis miring (\"/\") seperti \"/hello\" untuk mengaktifkannya."), + ("cancel-2fa-confirm-tip", "Apakah Kamu yakin ingin membatalkan 2FA?"), + ("cancel-bot-confirm-tip", "Apakah Kamu yakin ingin membatalkan bot Telegram?"), + ("About RustDesk", "Tentang RustDesk"), + ("Send clipboard keystrokes", "Kirim keystrokes clipboard"), + ("network_error_tip", "Periksa koneksi internet, lalu klik \"Coba lagi\"."), + ("Unlock with PIN", "Buka menggunakan PIN"), + ("Requires at least {} characters", "Memerlukan setidaknya {} karakter."), + ("Wrong PIN", "PIN salah"), + ("Set PIN", "Atur PIN"), + ("Enable trusted devices", "Izinkan perangkat tepercaya"), + ("Manage trusted devices", "Kelola perangkat tepercaya"), + ("Platform", "Platform"), + ("Days remaining", "Sisa hari"), + ("enable-trusted-devices-tip", "Tidak memerlukan verifikasi 2FA pada perangkat tepercaya."), + ("Parent directory", "Direktori utama"), + ("Resume", "Lanjutkan"), + ("Invalid file name", "Nama file tidak valid"), + ("one-way-file-transfer-tip", "Transfer file satu arah (One-way) telah diaktifkan pada sisi yang dikendalikan."), + ("Authentication Required", "Diperlukan autentikasi"), + ("Authenticate", "Autentikasi"), + ("web_id_input_tip", "Kamu bisa memasukkan ID pada server yang sama, akses IP langsung tidak didukung di klien web.\nJika Anda ingin mengakses perangkat di server lain, silakan tambahkan alamat server (@?key=), contohnya:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nUntuk mengakses perangkat di server publik, cukup masukkan \"@public\", tanpa kunci/key."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 7c18b52d2a1..e0dd83db6f7 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -73,7 +73,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember password", "Ricorda password"), ("Wrong Password", "Password errata"), ("Do you want to enter again?", "Vuoi riprovare?"), - ("Connection Error", "Errore di connessione"), + ("Connection Error", "Errore connessione"), ("Error", "Errore"), ("Reset by the peer", "Reimpostata dal dispositivo remoto"), ("Connecting...", "Connessione..."), @@ -130,18 +130,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), ("Lock after session end", "Blocca al termine della sessione"), - ("Insert", "Inserisci"), + ("Insert Ctrl + Alt + Del", "Inserisci Ctrl + Alt + Del"), ("Insert Lock", "Blocco inserimento"), ("Refresh", "Aggiorna"), ("ID does not exist", "L'ID non esiste"), - ("Failed to connect to rendezvous server", "Errore di connessione al server rendezvous"), + ("Failed to connect to rendezvous server", "Errore connessione al server rendezvous"), ("Please try later", "Riprova più tardi"), ("Remote desktop is offline", "Il desktop remoto è offline"), ("Key mismatch", "La chiave non corrisponde"), ("Timeout", "Timeout"), - ("Failed to connect to relay server", "Errore di connessione al server relay"), - ("Failed to connect via rendezvous server", "Errore di connessione tramite il server rendezvous"), - ("Failed to connect via relay server", "Errore di connessione tramite il server relay"), + ("Failed to connect to relay server", "Errore connessione al server relay"), + ("Failed to connect via rendezvous server", "Errore connessione tramite il server rendezvous"), + ("Failed to connect via relay server", "Errore connessione tramite il server relay"), ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), ("Set Password", "Imposta password"), ("OS Password", "Password sistema operativo"), @@ -226,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi etichetta"), ("Unselect all tags", "Deseleziona tutte le etichette"), - ("Network error", "Errore di rete"), + ("Network error", "Errore rete"), ("Username missed", "Nome utente mancante"), ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), @@ -363,13 +363,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Sblocca barra strumenti"), ("Recording", "Registrazione"), ("Directory", "Cartella"), - ("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"), + ("Automatically record incoming sessions", "Registra automaticamente sessioni in entrata"), + ("Automatically record outgoing sessions", "Registra automaticamente sessioni in uscita"), ("Change", "Modifica"), ("Start session recording", "Inizia registrazione sessione"), ("Stop session recording", "Ferma registrazione sessione"), ("Enable recording session", "Abilita registrazione sessione"), ("Enable LAN discovery", "Abilita rilevamento LAN"), - ("Deny LAN discovery", "Nega rilevamento LAN"), + ("Deny LAN discovery", "Non effettuare rilevamento LAN"), ("Write a message", "Scrivi un messaggio"), ("Prompt", "Richiedi"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), @@ -532,7 +533,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("HSV Color", "Colore HSV"), ("Installation Successful!", "Installazione completata"), ("Installation failed!", "Installazione fallita"), - ("Reverse mouse wheel", "Rotella mouse inversa"), + ("Reverse mouse wheel", "Funzione rotellina mouse inversa"), ("{} sessions", "{} sessioni"), ("scam_title", "Potresti essere stato TRUFFATO!"), ("scam_text1", "Se sei al telefono con qualcuno che NON conosci NON DI TUA FIDUCIA che ti ha chiesto di usare RustDesk e di avviare il servizio, non procedere e riattacca subito."), @@ -559,7 +560,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Big tiles", "Icone grandi"), ("Small tiles", "Icone piccole"), ("List", "Elenco"), - ("Virtual display", "Scehrmo virtuale"), + ("Virtual display", "Schermo virtuale"), ("Plug out all", "Scollega tutto"), ("True color (4:4:4)", "Colore reale (4:4:4)"), ("Enable blocking user input", "Abilita blocco input utente"), @@ -632,7 +633,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), - ("Unlock with PIN", "Sblocca con PIN"), + ("Unlock with PIN", "Abilita sblocco con PIN"), ("Requires at least {} characters", "Richiede almeno {} caratteri"), ("Wrong PIN", "PIN errato"), ("Set PIN", "Imposta PIN"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Cartella principale"), ("Resume", "Riprendi"), ("Invalid file name", "Nome file non valido"), + ("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."), + ("Authentication Required", "Richiesta autenticazione"), + ("Authenticate", "Autentica"), + ("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"@public\", la chiave non è necessaria per il server pubblico."), + ("Download", "Download"), + ("Upload folder", "Cartella upload"), + ("Upload files", "File upload"), + ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ffb93e379f7..5edd5057299 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -19,7 +19,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "最近のセッション"), ("Address book", "アドレス帳"), ("Confirmation", "確認"), - ("TCP tunneling", "TXPトンネリング"), + ("TCP tunneling", "TCPトンネリング"), ("Remove", "削除"), ("Refresh random password", "ランダムパスワードを再生成"), ("Set your own password", "パスワードを設定"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "品質モニターを表示"), ("Disable clipboard", "クリップボードを無効化"), ("Lock after session end", "セッション終了後にロックする"), - ("Insert", "送信"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 送信"), ("Insert Lock", "ロック命令を送信"), ("Refresh", "更新"), ("ID does not exist", "IDが存在しません"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "録画"), ("Directory", "ディレクトリ"), ("Automatically record incoming sessions", "受信したセッションを自動で記録する"), + ("Automatically record outgoing sessions", ""), ("Change", "変更"), ("Start session recording", "セッションの録画を開始"), ("Stop session recording", "セッションの録画を停止"), @@ -630,19 +631,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "本当に二要素認証をキャンセルしますか?"), ("cancel-bot-confirm-tip", "本当にTelegram Botをキャンセルしますか?"), ("About RustDesk", "RustDeskについて"), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Send clipboard keystrokes", "クリップボードの内容をキー入力として送信する"), + ("network_error_tip", "ネットワーク接続を確認し、再度お試しください。"), + ("Unlock with PIN", "PINでロック解除"), + ("Requires at least {} characters", "最低でも{}文字必要です"), + ("Wrong PIN", "PINが間違っています"), + ("Set PIN", "PINを設定"), + ("Enable trusted devices", "承認済デバイスを有効化"), + ("Manage trusted devices", "承認済デバイスの管理"), + ("Platform", "プラットフォーム"), + ("Days remaining", "残り日数"), + ("enable-trusted-devices-tip", "承認済デバイスで2FAチェックをスキップします。"), + ("Parent directory", "親ディレクトリ"), + ("Resume", "再開"), + ("Invalid file name", "無効なファイル名"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 3e84ca262f5..71bff511908 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "품질 모니터 보기"), ("Disable clipboard", "클립보드 비활성화"), ("Lock after session end", "세션 종료 후 화면 잠금"), - ("Insert", "입력"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 입력"), ("Insert Lock", "원격 입력 잠금"), ("Refresh", "새로고침"), ("ID does not exist", "ID가 존재하지 않습니다"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "녹화"), ("Directory", "경로"), ("Automatically record incoming sessions", "들어오는 세션을 자동으로 녹화"), + ("Automatically record outgoing sessions", ""), ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), @@ -641,8 +642,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "플랫폼"), ("Days remaining", "일 남음"), ("enable-trusted-devices-tip", "신뢰할 수 있는 기기에서 2FA 검증 건너뛰기"), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "상위 디렉토리"), + ("Resume", "재개"), + ("Invalid file name", "잘못된 파일 이름"), + ("one-way-file-transfer-tip", "단방향 파일 전송은 제어되는 쪽에서 활성화됩니다."), + ("Authentication Required", "인증 필요함"), + ("Authenticate", "인증"), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c47764c8190..07ca645f272 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Сапа мониторын көрсету"), ("Disable clipboard", "Көшіру-тақтасын өшіру"), ("Lock after session end", "Сеш аяқталған соң құлыптау"), - ("Insert", "Кірістіру"), + ("Insert Ctrl + Alt + Del", "Кірістіру Ctrl + Alt + Del"), ("Insert Lock", "Кірістіруді Құлыптау"), ("Refresh", "Жаңарту"), ("ID does not exist", "ID табылмады"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index b1d0317f89d..9a2069163ed 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Rodyti kokybės monitorių"), ("Disable clipboard", "Išjungti mainų sritį"), ("Lock after session end", "Užrakinti pasibaigus seansui"), - ("Insert", "Įdėti"), + ("Insert Ctrl + Alt + Del", "Įdėti Ctrl + Alt + Del"), ("Insert Lock", "Įterpti užraktą"), ("Refresh", "Atnaujinti"), ("ID does not exist", "ID neegzistuoja"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Įrašymas"), ("Directory", "Katalogas"), ("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"), + ("Automatically record outgoing sessions", ""), ("Change", "Keisti"), ("Start session recording", "Pradėti seanso įrašinėjimą"), ("Stop session recording", "Sustabdyti seanso įrašinėjimą"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index df2324def57..e8ba903dfb6 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Rādīt kvalitātes monitoru"), ("Disable clipboard", "Atspējot starpliktuvi"), ("Lock after session end", "Bloķēt pēc sesijas beigām"), - ("Insert", "Ievietot"), + ("Insert Ctrl + Alt + Del", "Ievietot Ctrl + Alt + Del"), ("Insert Lock", "Ievietot Bloķēt"), ("Refresh", "Atsvaidzināt"), ("ID does not exist", "ID neeksistē"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Ierakstīšana"), ("Directory", "Direktorija"), ("Automatically record incoming sessions", "Automātiski ierakstīt ienākošās sesijas"), + ("Automatically record outgoing sessions", ""), ("Change", "Mainīt"), ("Start session recording", "Sākt sesijas ierakstīšanu"), ("Stop session recording", "Apturēt sesijas ierakstīšanu"), @@ -629,20 +630,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1. Atveriet tērzēšanu ar @BotFather.\n2. Nosūtiet komandu \"/newbot\". Pēc šīs darbības pabeigšanas jūs saņemsit pilnvaru.\n3. Sāciet tērzēšanu ar jaunizveidoto robotprogrammatūru. Lai to aktivizētu, nosūtiet ziņojumu, kas sākas ar slīpsvītru (\"/\"), piemēram, \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Vai tiešām vēlaties atcelt 2FA?"), ("cancel-bot-confirm-tip", "Vai tiešām vēlaties atcelt Telegram robotu?"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("About RustDesk", "Par RustDesk"), + ("Send clipboard keystrokes", "Nosūtīt starpliktuves taustiņsitienus"), + ("network_error_tip", "Lūdzu, pārbaudiet tīkla savienojumu un pēc tam noklikšķiniet uz Mēģināt vēlreiz."), + ("Unlock with PIN", "Atbloķēt ar PIN"), + ("Requires at least {} characters", "Nepieciešamas vismaz {} rakstzīmes"), + ("Wrong PIN", "Nepareizs PIN"), + ("Set PIN", "Iestatīt PIN"), + ("Enable trusted devices", "Iespējot uzticamas ierīces"), + ("Manage trusted devices", "Pārvaldīt uzticamas ierīces"), + ("Platform", "Platforma"), + ("Days remaining", "Atlikušas dienas"), + ("enable-trusted-devices-tip", "Izlaist 2FA verifikāciju uzticamās ierīcēs"), + ("Parent directory", "Vecākdirektorijs"), + ("Resume", "Atsākt"), + ("Invalid file name", "Nederīgs faila nosaukums"), + ("one-way-file-transfer-tip", "Kontrolējamajā pusē ir iespējota vienvirziena failu pārsūtīšana."), + ("Authentication Required", "Nepieciešama autentifikācija"), + ("Authenticate", "Autentificēt"), + ("web_id_input_tip", "Varat ievadīt ID tajā pašā serverī, tīmekļa klientā tiešā IP piekļuve netiek atbalstīta.\nJa vēlaties piekļūt ierīcei citā serverī, lūdzu, pievienojiet servera adresi (@?key=), piemēram,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJa vēlaties piekļūt ierīcei publiskajā serverī, lūdzu, ievadiet \"@public\", publiskajam serverim atslēga nav nepieciešama."), + ("Download", "Lejupielādēt"), + ("Upload folder", "Augšupielādēt mapi"), + ("Upload files", "Augšupielādēt failus"), + ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d23191ef2b7..a91e31e45be 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Vis bildekvalitet"), ("Disable clipboard", "Deaktiver utklipstavle"), ("Lock after session end", "Lås etter avsluttet fjernstyring"), - ("Insert", "Sett inn"), + ("Insert Ctrl + Alt + Del", "Sett inn Ctrl + Alt + Del"), ("Insert Lock", "Sett inn lås"), ("Refresh", "Oppdater"), ("ID does not exist", "ID finnes ikke"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Opptak"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Ta opp innkommende sesjoner automatisk"), + ("Automatically record outgoing sessions", ""), ("Change", "Rediger"), ("Start session recording", "Start sesjonsopptak"), ("Stop session recording", "Stopp sesjonsopptak"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 9ae7d0839f9..ca46a32853e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -31,8 +31,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID/Relay Server", "ID/Relay Server"), ("Import server config", "Importeer Serverconfiguratie"), ("Export Server Config", "Exporteer Serverconfiguratie"), - ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), - ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Import server configuration successfully", "Importeren serverconfiguratie is geslaagd"), + ("Export server configuration successfully", "Exporteren serverconfiguratie is geslaagd"), ("Invalid server configuration", "Ongeldige Serverconfiguratie"), ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), @@ -80,7 +80,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), ("Please try 1 minute later", "Probeer 1 minuut later"), ("Login Error", "Login Fout"), - ("Successful", "Succesvol"), + ("Successful", "Geslaagd"), ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), ("Name", "Naam"), ("Type", "Type"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Kwaliteitsmonitor tonen"), ("Disable clipboard", "Klembord uitschakelen"), ("Lock after session end", "Vergrendelen na einde sessie"), - ("Insert", "Invoegen"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoegen"), ("Insert Lock", "Vergrendeling Invoegen"), ("Refresh", "Vernieuwen"), ("ID does not exist", "ID bestaat niet"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Opnemen"), ("Directory", "Map"), ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Automatically record outgoing sessions", ""), ("Change", "Wissel"), ("Start session recording", "Start de sessieopname"), ("Stop session recording", "Stop de sessieopname"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Hoofdmap"), ("Resume", "Hervatten"), ("Invalid file name", "Ongeldige bestandsnaam"), + ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), + ("Authentication Required", "Verificatie vereist"), + ("Authenticate", "Verificatie"), + ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls je toegang wilt tot een apparaat op een andere server, voeg je het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), + ("Download", "Downloaden"), + ("Upload folder", "Map uploaden"), + ("Upload files", "Bestanden uploaden"), + ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 246de02f4a7..fd5641ac133 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), - ("Insert", "Wyślij"), + ("Insert Ctrl + Alt + Del", "Wyślij Ctrl + Alt + Del"), ("Insert Lock", "Zablokuj zdalne urządzenie"), ("Refresh", "Odśwież"), ("ID does not exist", "ID nie istnieje"), @@ -349,7 +349,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), ("Enable audio", "Włącz dźwięk"), - ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), + ("Unlock Network Settings", "Odblokuj ustawienia sieciowe"), ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni adres IP"), ("Proxy", "Proxy"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nagrywanie"), ("Directory", "Folder"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), + ("Automatically record outgoing sessions", "Automatycznie nagrywaj sesje wychodzące"), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), @@ -602,47 +603,55 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Brak fizycznych wyświetlaczy, tryb prywatny nie jest potrzebny."), ("Follow remote cursor", "Podążaj za zdalnym kursorem"), ("Follow remote window focus", "Podążaj za aktywnością zdalnych okien"), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("default_proxy_tip", "Domyślny protokół i port to Socks5 i 1080"), + ("no_audio_input_device_tip", "Nie znaleziono urządzenia audio."), + ("Incoming", "Przychodzące"), + ("Outgoing", "Wychodzące"), + ("Clear Wayland screen selection", "Wyczyść wybór ekranu Wayland"), + ("clear_Wayland_screen_selection_tip", "Po wyczyszczeniu wyboru ekranu, możesz wybrać, który ekran chcesz udostępnić."), + ("confirm_clear_Wayland_screen_selection_tip", "Na pewno wyczyścić wybór ekranu Wayland?"), + ("android_new_voice_call_tip", "Otrzymano nowe żądanie połączenia głosowego. Jeżeli je zaakceptujesz, dźwięk przełączy się na komunikację głosową."), + ("texture_render_tip", "Użyj renderowania tekstur, aby wygładzić zdjęcia. Możesz spróbować wyłączyć tę opcję, jeżeli napotkasz problemy z renderowaniem."), + ("Use texture rendering", "Użyj renderowania tekstur"), + ("Floating window", "Okno pływające"), + ("floating_window_tip", "Pozwala zachować usługę RustDesk w tle"), + ("Keep screen on", "Pozostaw ekran włączony"), + ("Never", "Nigdy"), + ("During controlled", "Podczas sterowania"), + ("During service is on", "Gdy usługa jest uruchomiona"), + ("Capture screen using DirectX", "Przechwytuj ekran używając DirectX"), + ("Back", "Wstecz"), + ("Apps", "Aplikacje"), + ("Volume up", "Głośniej"), + ("Volume down", "Ciszej"), + ("Power", "Zasilanie"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Jeżeli włączysz tę funkcję, możesz otrzymać kod 2FA od swojego bota. Może on również działać jako powiadomienie o połączeniu."), + ("enable-bot-desc", "1. Otwórz czat z @BotFather.\n2. Wyślij polecenie \"/newbot\". Otrzymasz token do po wykonaniu tego kroku.\n3. Rozpocznij czat z nowo utworzonym botem. Wyślij wiadomość zaczynającą się od ukośnika (\"/\"),np. \"/hello\", aby go aktywować.\n"), + ("cancel-2fa-confirm-tip", "Na pewno chcesz anulować 2FA?"), + ("cancel-bot-confirm-tip", "Na pewno chcesz anulować bot Telegram?"), + ("About RustDesk", "O programie"), + ("Send clipboard keystrokes", "Wysyła naciśnięcia klawiszy ze schowka"), + ("network_error_tip", "Sprawdź swoje połączenie sieciowe, następnie kliknij Ponów."), + ("Unlock with PIN", "Odblokuj za pomocą PIN"), + ("Requires at least {} characters", "Wymaga co najmniej {} znaków"), + ("Wrong PIN", "Niewłaściwy PIN"), + ("Set PIN", "Ustaw PIN"), + ("Enable trusted devices", "Włącz zaufane urządzenia"), + ("Manage trusted devices", "Zarządzaj zaufanymi urządzeniami"), + ("Platform", "Platforma"), + ("Days remaining", "Pozostało dni"), + ("enable-trusted-devices-tip", "Omiń weryfikację 2FA dla zaufanych urządzeń"), + ("Parent directory", "Folder nadrzędny"), + ("Resume", "Wznów"), + ("Invalid file name", "Nieprawidłowa nazwa pliku"), + ("one-way-file-transfer-tip", "Jednokierunkowy transfer plików jest włączony po stronie kontrolowanej."), + ("Authentication Required", "Wymagana autoryzacja"), + ("Authenticate", "Uwierzytelnienie"), + ("web_id_input_tip", "Jeśli chcesz uzyskać dostęp do urządzenia na innym serwerze, dodaj adres serwera (@?key=) na przykład, \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJeśli chcesz uzyskać dostęp do urządzenia na serwerze publicznym, wprowadź \"@public\", klucz nie jest wymagany dla serwera publicznego."), + ("Download", "Pobierz"), + ("Upload folder", "Wyślij folder"), + ("Upload files", "Wyślij pliki"), + ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6afa4c528eb..c0564e0f401 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), - ("Insert", "Inserir"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), ("Insert Lock", "Bloquear Inserir"), ("Refresh", "Actualizar"), ("ID does not exist", "ID não existente"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 83ee1e0d233..14254388cc1 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Exibir monitor de qualidade"), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), - ("Insert", "Inserir"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), ("Insert Lock", "Bloquear computador"), ("Refresh", "Atualizar"), ("ID does not exist", "ID não existe"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Gravando"), ("Directory", "Diretório"), ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), + ("Automatically record outgoing sessions", ""), ("Change", "Alterar"), ("Start session recording", "Iniciar gravação da sessão"), ("Stop session recording", "Parar gravação da sessão"), @@ -498,131 +499,131 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("resolution_original_tip", "Resolução original"), ("resolution_fit_local_tip", "Adequar a resolução local"), ("resolution_custom_tip", "Customizar resolução"), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), + ("Collapse toolbar", "Ocultar barra de ferramentas"), + ("Accept and Elevate", "Aceitar e elevar"), + ("accept_and_elevate_btn_tooltip", "Aceitar a conexão e elevar os privilégios do UAC."), + ("clipboard_wait_response_timeout_tip", "Tempo de espera para a resposta da área de transferência expirado."), + ("Incoming connection", "Conexão de entrada"), + ("Outgoing connection", "Conexão de saída"), ("Exit", "Sair"), ("Open", "Abrir"), - ("logout_tip", ""), + ("logout_tip", "Tem certeza que deseja sair?"), ("Service", "Serviço"), ("Start", "Iniciar"), ("Stop", "Parar"), - ("exceed_max_devices", ""), + ("exceed_max_devices", "Você atingiu o número máximo de dispositivos gerenciados."), ("Sync with recent sessions", "Sincronizar com sessões recentes"), ("Sort tags", "Classificar tags"), ("Open connection in new tab", "Abrir conexão em uma nova aba"), ("Move tab to new window", "Mover aba para uma nova janela"), ("Can not be empty", "Não pode estar vazio"), - ("Already exists", ""), + ("Already exists", "Já existe"), ("Change Password", "Alterar senha"), ("Refresh Password", "Atualizar senha"), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), + ("ID", "ID"), + ("Grid View", "Visualização em grade"), + ("List View", "Visualização em lista"), + ("Select", "Selecionar"), + ("Toggle Tags", "Alternar etiquetas"), + ("pull_ab_failed_tip", "Não foi possível atualizar o diretório"), + ("push_ab_failed_tip", "Não foi possível sincronizar o diretório com o servidor"), + ("synced_peer_readded_tip", "Os dispositivos presentes em sessões recentes serão sincronizados com o diretório."), ("Change Color", "Alterar cor"), ("Primary Color", "Cor principal"), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), + ("HSV Color", "Cor HSV"), + ("Installation Successful!", "Instalação bem-sucedida!"), + ("Installation failed!", "A instalação falhou!"), + ("Reverse mouse wheel", "Inverter rolagem do mouse"), ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), + ("scam_title", "Você pode estar sendo ENGANADO!"), + ("scam_text1", "Se você estiver ao telefone com alguém que NÃO conhece e em quem NÃO confia e essa pessoa pedir para você usar o RustDesk e iniciar o serviço, NÃO faça isso !! e desligue imediatamente."), + ("scam_text2", "Provavelmente são golpistas tentando roubar seu dinheiro ou informações privadas."), + ("Don't show again", "Não mostrar novamente"), ("I Agree", "Eu concordo"), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), + ("Decline", "Recusar"), + ("Timeout in minutes", "Tempo limite em minutos"), + ("auto_disconnect_option_tip", "Encerrar sessões entrantes automaticamente por inatividade do usuário."), + ("Connection failed due to inactivity", "Conexão encerrada automaticamente por inatividade."), + ("Check for software update on startup", "Verificar atualizações do software ao iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualize o RustDesk Server Pro para a versão {} ou superior."), + ("pull_group_failed_tip", "Não foi possível atualizar o grupo."), ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), ("Test", "Teste"), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), + ("display_is_plugged_out_msg", "A tela está desconectada. Mudando para a principal."), + ("No displays", "Nenhum display encontrado"), + ("Open in new window", "Abrir em uma nova janela"), + ("Show displays as individual windows", "Mostrar as telas como janelas individuais"), + ("Use all my displays for the remote session", "Usar todas as minhas telas para a sessão remota"), + ("selinux_tip", "O SELinux está ativado em seu dispositivo, o que pode impedir que o RustDesk funcione corretamente como dispositivo controlado."), ("Change view", "Alterar visualização"), - ("Big tiles", ""), - ("Small tiles", ""), + ("Big tiles", "Ícones grandes"), + ("Small tiles", "Ícones pequenos"), ("List", "Lista"), ("Virtual display", "Display Virtual"), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), + ("Plug out all", "Desconectar tudo"), + ("True color (4:4:4)", "Cor verdadeira (4:4:4)"), + ("Enable blocking user input", "Habilitar bloqueio da entrada do usuário"), + ("id_input_tip", "Você pode inserir um ID, um IP direto ou um domínio com uma porta (:).\nPara acessar um dispositivo em outro servidor, adicione o IP do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nPara acessar um dispositivo em um servidor público, insira \"@public\", a chave não é necessária para um servidor público."), ("privacy_mode_impl_mag_tip", ""), ("privacy_mode_impl_virtual_display_tip", ""), ("Enter privacy mode", "Entrar no modo privado"), ("Exit privacy mode", "Sair do modo privado"), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), + ("idd_not_support_under_win10_2004_tip", "O driver de tela indireto não é suportado. É necessário o Windows 10, versão 2004 ou superior."), + ("input_source_1_tip", "Fonte de entrada 1"), + ("input_source_2_tip", "Fonte de entrada 2"), + ("Swap control-command key", "Trocar teclas Control e Comando"), + ("swap-left-right-mouse", "Trocar botões esquerdo e direito do mouse"), ("2FA code", "Código 2FA"), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), + ("More", "Mais"), + ("enable-2fa-title", "Habilitar autenticação em duas etapas"), + ("enable-2fa-desc", "Configure seu autenticador agora. Você pode usar um aplicativo de autenticação como Authy, Microsoft ou Google Authenticator em seu telefone ou computador. Escaneie o código QR com seu aplicativo e insira o código mostrado para habilitar a autenticação em duas etapas."), + ("wrong-2fa-code", "Código inválido. Verifique se o código e as configurações de horário estão corretas."), + ("enter-2fa-title", "Autenticação em duas etapas"), + ("Email verification code must be 6 characters.", "O código de verificação por e-mail deve ter 6 caracteres."), + ("2FA code must be 6 digits.", "O código 2FA deve ter 6 dígitos."), + ("Multiple Windows sessions found", "Múltiplas sessões de janela encontradas"), ("Please select the session you want to connect to", "Por favor, selecione a sessão que você deseja se conectar"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), + ("powered_by_me", "Desenvolvido por RustDesk"), + ("outgoing_only_desk_tip", "Esta é uma edição personalizada.\nVocê pode se conectar a outros dispositivos, mas eles não podem se conectar ao seu."), + ("preset_password_warning", "Atenção: esta edição personalizada vem com uma senha predefinida. Qualquer pessoa que a conhecer poderá controlar totalmente seu dispositivo. Se isso não for o que você deseja, desinstale o software imediatamente."), + ("Security Alert", "Alerta de Segurança"), + ("My address book", "Minha lista de contatos"), ("Personal", ""), - ("Owner", ""), + ("Owner", "Proprietário"), ("Set shared password", "Definir senha compartilhada"), ("Exist in", ""), ("Read-only", "Apenas leitura"), ("Read/Write", "Leitura/escrita"), ("Full Control", "Controle total"), - ("share_warning_tip", ""), + ("share_warning_tip", "Os campos mostrados acima são compartilhados e visíveis por outras pessoas."), ("Everyone", "Todos"), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("ab_web_console_tip", "Mais opções no console web"), + ("allow-only-conn-window-open-tip", "Permitir conexões apenas quando a janela do RustDesk estiver aberta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Sem telas físicas, o modo privado não é necessário"), + ("Follow remote cursor", "Seguir cursor remoto"), + ("Follow remote window focus", "Seguir janela remota ativa"), ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("no_audio_input_device_tip", "Nenhum dispositivo de entrada de áudio encontrado"), ("Incoming", ""), ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), + ("Clear Wayland screen selection", "Limpar seleção de tela do Wayland"), + ("clear_Wayland_screen_selection_tip", "Depois de limpar a seleção de tela, você pode selecioná-la novamente para compartilhar."), + ("confirm_clear_Wayland_screen_selection_tip", "Tem certeza que deseja limpar a seleção da tela do Wayland?"), ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), + ("texture_render_tip", "Use renderização de textura para tornar as imagens mais suaves"), + ("Use texture rendering", "Usar renderização de textura"), + ("Floating window", "Janela flutuante"), + ("floating_window_tip", "Ajuda a manter o serviço RustDesk em segundo plano"), + ("Keep screen on", "Manter tela ligada"), + ("Never", "Nunca"), + ("During controlled", "Durante controle"), + ("During service is on", "Enquanto o serviço estiver ativo"), + ("Capture screen using DirectX", "Capturar tela usando DirectX"), ("Back", "Voltar"), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), + ("Apps", "Apps"), + ("Volume up", "Aumentar volume"), + ("Volume down", "Diminuir volume"), ("Power", ""), ("Telegram bot", "Bot Telegram"), ("enable-bot-tip", "Se você ativar este recurso, poderá receber o código 2FA do seu bot. Ele também pode funcionar como uma notificação de conexão."), @@ -630,19 +631,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Tem certeza de que deseja cancelar a 2FA?"), ("cancel-bot-confirm-tip", "Tem certeza de que deseja cancelar o bot do Telegram?"), ("About RustDesk", "Sobre RustDesk"), - ("Send clipboard keystrokes", ""), + ("Send clipboard keystrokes", "Colar área de transferência"), ("network_error_tip", "Por favor, verifique sua conexão de rede e clique em tentar novamente."), ("Unlock with PIN", "Desbloquear com PIN"), - ("Requires at least {} characters", ""), + ("Requires at least {} characters", "São necessários pelo menos {} caracteres"), ("Wrong PIN", "PIN Errado"), ("Set PIN", "Definir PIN"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Enable trusted devices", "Habilitar dispositivos confiáveis"), + ("Manage trusted devices", "Gerenciar dispositivos confiáveis"), + ("Platform", "Plataforma"), + ("Days remaining", "Dias restantes"), + ("enable-trusted-devices-tip", "Ignore a verificação de dois fatores em dispositivos confiáveis"), + ("Parent directory", "Diretório pai"), + ("Resume", "Continuar"), + ("Invalid file name", "Nome de arquivo inválido"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 0f11e544953..cbce2f2a927 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afișează detalii despre conexiune"), ("Disable clipboard", "Dezactivează clipboard"), ("Lock after session end", "Blochează după deconectare"), - ("Insert", "Introdu"), + ("Insert Ctrl + Alt + Del", "Introdu Ctrl + Alt + Del"), ("Insert Lock", "Blochează computer"), ("Refresh", "Reîmprospătează"), ("ID does not exist", "ID neexistent"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Înregistrare"), ("Directory", "Director"), ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifică"), ("Start session recording", "Începe înregistrarea"), ("Stop session recording", "Oprește înregistrarea"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index dc4b2133f9e..6d173f1097b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показывать монитор качества"), ("Disable clipboard", "Отключить буфер обмена"), ("Lock after session end", "Заблокировать учётную запись после сеанса"), - ("Insert", "Вставить"), + ("Insert Ctrl + Alt + Del", "Вставить Ctrl + Alt + Del"), ("Insert Lock", "Заблокировать учётную запись"), ("Refresh", "Обновить"), ("ID does not exist", "ID не существует"), @@ -147,7 +147,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль входа в ОС"), ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать возможных проблем с UAC, нажмите кнопку ниже для установки RustDesk в системе."), ("Click to upgrade", "Нажмите, чтобы обновить"), - ("Click to download", "Нажмите, чтобы загрузить"), + ("Click to download", "Нажмите, чтобы скачать"), ("Click to update", "Нажмите, чтобы обновить"), ("Configure", "Настроить"), ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запись"), ("Directory", "Папка"), ("Automatically record incoming sessions", "Автоматически записывать входящие сеансы"), + ("Automatically record outgoing sessions", "Автоматически записывать исходящие сеансы"), ("Change", "Изменить"), ("Start session recording", "Начать запись сеанса"), ("Stop session recording", "Остановить запись сеанса"), @@ -554,7 +555,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Open in new window", "Открыть в новом окне"), ("Show displays as individual windows", "Показывать дисплеи в отдельных окнах"), ("Use all my displays for the remote session", "Использовать все мои дисплеи для удалённого сеанса"), - ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на контролируемой стороне."), + ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на управляемой стороне."), ("Change view", "Вид"), ("Big tiles", "Большие значки"), ("Small tiles", "Маленькие значки"), @@ -636,13 +637,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "Требуется не менее {} символов"), ("Wrong PIN", "Неправильный PIN-код"), ("Set PIN", "Установить PIN-код"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Enable trusted devices", "Включение доверенных устройств"), + ("Manage trusted devices", "Управление доверенными устройствами"), + ("Platform", "Платформа"), + ("Days remaining", "Дней осталось"), + ("enable-trusted-devices-tip", "Разрешить доверенным устройствам пропускать проверку подлинности 2FA"), + ("Parent directory", "Родительская папка"), + ("Resume", "Продолжить"), + ("Invalid file name", "Неправильное имя файла"), + ("one-way-file-transfer-tip", "На управляемой стороне включена односторонняя передача файлов."), + ("Authentication Required", "Требуется аутентификация"), + ("Authenticate", "Аутентификация"), + ("web_id_input_tip", "Можно ввести ID на том же сервере, прямой доступ по IP в веб-клиенте не поддерживается.\nЕсли вы хотите получить доступ к устройству на другом сервере, добавьте адрес сервера (@<адрес_сервера>?key=<ключ>), например,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕсли вы хотите получить доступ к устройству на публичном сервере, введите \"@public\", для публичного сервера ключ не нужен."), + ("Download", "Скачать"), + ("Upload folder", "Загрузить папку"), + ("Upload files", "Загрузить файлы"), + ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d1f5467afaa..b3c8fddf916 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Zobraziť monitor kvality"), ("Disable clipboard", "Vypnúť schránku"), ("Lock after session end", "Po skončení uzamknúť plochu"), - ("Insert", "Vložiť"), + ("Insert Ctrl + Alt + Del", "Vložiť Ctrl + Alt + Del"), ("Insert Lock", "Uzamknúť"), ("Refresh", "Aktualizovať"), ("ID does not exist", "ID neexistuje"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Nahrávanie"), ("Directory", "Adresár"), ("Automatically record incoming sessions", "Automaticky nahrávať prichádzajúce relácie"), + ("Automatically record outgoing sessions", ""), ("Change", "Zmeniť"), ("Start session recording", "Spustiť záznam relácie"), ("Stop session recording", "Zastaviť záznam relácie"), @@ -632,17 +633,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "O RustDesk"), ("Send clipboard keystrokes", "Odoslať stlačenia klávesov zo schránky"), ("network_error_tip", "Skontrolujte svoje sieťové pripojenie a potom kliknite na tlačidlo Opakovať."), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Unlock with PIN", "Odomknutie pomocou PIN kódu"), + ("Requires at least {} characters", "Vyžaduje aspoň {} znakov"), + ("Wrong PIN", "Nesprávny PIN kód"), + ("Set PIN", "Nastavenie PIN kódu"), + ("Enable trusted devices", "Povolenie dôveryhodných zariadení"), + ("Manage trusted devices", "Správa dôveryhodných zariadení"), + ("Platform", "Platforma"), + ("Days remaining", "Zostávajúce dni"), + ("enable-trusted-devices-tip", "Vynechanie overovania 2FA na dôveryhodných zariadeniach"), + ("Parent directory", "Rodičovský adresár"), + ("Resume", "Obnoviť"), + ("Invalid file name", "Nesprávny názov súboru"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 7c8d9749486..20fd24c9ca0 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -37,19 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Odložišče je prazno"), ("Stop service", "Ustavi storitev"), ("Change ID", "Spremeni ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Vaš nov ID"), + ("length %min% to %max%", "dolžina od %min% do %max%"), + ("starts with a letter", "začne se s črko"), + ("allowed characters", "dovoljeni znaki"), ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "Izjava o zasebnosti"), ("Mute", "Izklopi zvok"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Datum graditve"), + ("Version", "Različica"), + ("Home", "Začetek"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), ("Hardware Codec", "Strojni kodek"), @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži nadzornik kakovosti"), ("Disable clipboard", "Onemogoči odložišče"), ("Lock after session end", "Zakleni ob koncu seje"), - ("Insert", "Vstavi"), + ("Insert Ctrl + Alt + Del", "Vstavi Ctrl + Alt + Del"), ("Insert Lock", "Zakleni oddaljeni računalnik"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne obstaja"), @@ -210,27 +210,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), ("Run without install", "Zaženi brez namestitve"), - ("Connect via relay", ""), + ("Connect via relay", "Poveži preko posrednika"), ("Always connect via relay", "Vedno poveži preko posrednika"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("Login", "Prijavi"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Preveri"), + ("Remember me", "Zapomni si me"), + ("Trust this device", "Zaupaj tej napravi"), + ("Verification code", "Koda za preverjanje"), + ("verification_tip", "Kodo za preverjanje prejmete na registrirani e-poštni naslov"), ("Logout", "Odjavi"), ("Tags", "Oznake"), ("Search ID", "Išči ID"), ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznako"), - ("Unselect all tags", ""), + ("Unselect all tags", "Odznači vse oznake"), ("Network error", "Omrežna napaka"), ("Username missed", "Up. ime izpuščeno"), ("Password missed", "Geslo izpuščeno"), ("Wrong credentials", "Napačne poverilnice"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Koda za preverjanje je napačna, ali pa je potekla"), ("Edit Tag", "Uredi oznako"), ("Forget Password", "Pozabi geslo"), ("Favorites", "Priljubljene"), @@ -248,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), ("Download new version", "Prenesi novo različico"), ("Touch mode", "Način dotika"), - ("Mouse mode", "Način mišle"), + ("Mouse mode", "Način miške"), ("One-Finger Tap", "Tap z enim prstom"), ("Left Mouse", "Leva tipka miške"), ("One-Long Tap", "Dolg tap z enim prstom"), @@ -286,8 +286,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Tapnite [Zaženi storitev] ali pa omogočite pravico [Zajemanje zaslona] za zagon storitve deljenja zaslona."), + ("android_permission_may_not_change_tip", "Pravic za že vzpostavljene povezave ne morete spremeniti brez ponovne vzpostavitve povezave."), ("Account", "Račun"), ("Overwrite", "Prepiši"), ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), @@ -306,8 +306,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on boot", "Zaženi ob vklopu"), + ("Start the screen sharing service on boot, requires special permissions", "Zaženi storitev deljenja zaslona ob vklopu, zahteva posebna dovoljenja"), ("Connection not allowed", "Povezava ni dovoljena"), ("Legacy mode", "Stari način"), ("Map mode", "Način preslikave"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Razmerje"), ("Image Quality", "Kakovost slike"), ("Scroll Style", "Način drsenja"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Prikaži orodno vrstico"), + ("Hide Toolbar", "Skrij orodno vrstico"), ("Direct Connection", "Neposredna povezava"), ("Relay Connection", "Posredovana povezava"), ("Secure Connection", "Zavarovana povezava"), @@ -342,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Varnost"), ("Theme", "Tema"), ("Dark Theme", "Temna tema"), - ("Light Theme", ""), + ("Light Theme", "Svetla tema"), ("Dark", "Temna"), ("Light", "Svetla"), ("Follow System", "Sistemska"), @@ -359,11 +359,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Vhodna naprava za zvok"), ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), ("Network", "Mreža"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Pripni orodno vrstico"), + ("Unpin Toolbar", "Odpni orodno vrstico"), ("Recording", "Snemanje"), ("Directory", "Imenik"), ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Automatically record outgoing sessions", ""), ("Change", "Spremeni"), ("Start session recording", "Začni snemanje seje"), ("Stop session recording", "Ustavi snemanje seje"), @@ -409,240 +410,248 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), ("Local keyboard type", "Lokalna vrsta tipkovnice"), ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), - ("logout_tip", ""), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("software_render_tip", "Če na Linuxu uporabljate Nvidino grafično kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa."), + ("Always use software rendering", "Vedno uporabi programsko upodabljanje"), + ("config_input", "Za nadzor oddaljenega namizja s tipkovnico, rabi RustDesk pravico »Nadzor vnosa«."), + ("config_microphone", "Za zajem zvoka, rabi RustDesk pravico »Snemanje zvoka«."), + ("request_elevation_tip", "Lahko tudi zaprosite za dvig pravic, če je kdo na oddaljeni strani."), + ("Wait", "Čakaj"), + ("Elevation Error", "Napaka pri povzdigovanju"), + ("Ask the remote user for authentication", "Vprašaj oddaljenega uporabnika za prijavo"), + ("Choose this if the remote account is administrator", "Izberite to, če ima oddaljeni uporabnik skrbniške pravice"), + ("Transmit the username and password of administrator", "Vnesite poverilnice za skrbnika"), + ("still_click_uac_tip", "Oddaljeni uporabnik mora klikniti »Da« v oknu za nadzor uporabniškega računa."), + ("Request Elevation", "Zahtevaj povzdig pravic"), + ("wait_accept_uac_tip", "Počakajte na potrditev oddaljenega uporabnika v oknu za nadzor uporabniškega računa."), + ("Elevate successfully", "Povzdig pravic uspešen"), + ("uppercase", "velike črke"), + ("lowercase", "male črke"), + ("digit", "številke"), + ("special character", "posebni znaki"), + ("length>=8", "dolžina>=8"), + ("Weak", "Šibko"), + ("Medium", "Srednje"), + ("Strong", "Močno"), + ("Switch Sides", "Zamenjaj strani"), + ("Please confirm if you want to share your desktop?", "Potrdite, če želite deliti vaše namizje"), + ("Display", "Zaslon"), + ("Default View Style", "Privzeti način prikaza"), + ("Default Scroll Style", "Privzeti način drsenja"), + ("Default Image Quality", "Privzeta kakovost slike"), + ("Default Codec", "Privzeti kodek"), + ("Bitrate", "Bitna hitrost"), + ("FPS", "Sličice/sekundo"), + ("Auto", "Samodejno"), + ("Other Default Options", "Ostale privzete možnosti"), + ("Voice call", "Glasovni klic"), + ("Text chat", "Besedilni klepet"), + ("Stop voice call", "Prekini glasovni klic"), + ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poikusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."), + ("Reconnect", "Ponovna povezava"), + ("Codec", "Kodek"), + ("Resolution", "Ločljivost"), + ("No transfers in progress", "Trenutno ni prenosov"), + ("Set one-time password length", "Nastavi dolžino enkratnega gesla"), + ("RDP Settings", "Nastavitve za RDP"), + ("Sort by", "Razvrsti po"), + ("New Connection", "Nova povezava"), + ("Restore", "Obnovi"), + ("Minimize", "Minimiziraj"), + ("Maximize", "Maksimiziraj"), + ("Your Device", "Vaša naprava"), + ("empty_recent_tip", "Oops, ni nedavnih sej.\nPripravite novo."), + ("empty_favorite_tip", "Nimate še priljubljenih partnerjev?\nVzpostavite povezavo, in jo dodajte med priljubljene."), + ("empty_lan_tip", "Nismo našli še nobenih partnerjev."), + ("empty_address_book_tip", "Vaš adresar je prazen."), + ("eg: admin", "npr. admin"), + ("Empty Username", "Prazno uporabniško ime"), + ("Empty Password", "Prazno geslo"), + ("Me", "Jaz"), + ("identical_file_tip", "Datoteka je enaka partnerjevi"), + ("show_monitors_tip", "Prikaži monitorje v orodni vrstici"), + ("View Mode", "Način prikazovanja"), + ("login_linux_tip", "Prijaviti se morate v oddaljeni Linux račun in omogočiti namizno sejo X."), + ("verify_rustdesk_password_tip", "Preveri geslo za RustDesk"), + ("remember_account_tip", "Zapomni si ta račun"), + ("os_account_desk_tip", "Ta račun se uporabi za prijavo v oddaljeni sistem in omogči namizno sejo v napravi brez monitorja."), + ("OS Account", "Račun operacijskega sistema"), + ("another_user_login_title_tip", "Prijavljen je že drug uporabnik"), + ("another_user_login_text_tip", "Prekini"), + ("xorg_not_found_title_tip", "Xorg ni najden"), + ("xorg_not_found_text_tip", "Namestite Xorg"), + ("no_desktop_title_tip", "Namizno okolje ni na voljo"), + ("no_desktop_text_tip", "Namestite GNOME"), + ("No need to elevate", "Povzdig pravic ni potreben"), + ("System Sound", "Sistemski zvok"), + ("Default", "Privzeto"), + ("New RDP", "Nova RDP povezava"), + ("Fingerprint", "Prstni odtis"), + ("Copy Fingerprint", "Kopiraj prstni odtis"), + ("no fingerprints", "ni prstnega odtisa"), + ("Select a peer", "Izberite partnerja"), + ("Select peers", "Izberite partnerje"), + ("Plugins", "Vključki"), + ("Uninstall", "Odstrani"), + ("Update", "Posodobi"), + ("Enable", "Omogoči"), + ("Disable", "Onemogoči"), + ("Options", "Možnosti"), + ("resolution_original_tip", "Izvirna ločljivost"), + ("resolution_fit_local_tip", "Prilagodi lokalni ločljivosti"), + ("resolution_custom_tip", "Ločljivost po meri"), + ("Collapse toolbar", "Strni orodno vrstico"), + ("Accept and Elevate", "Sprejmi in povzdigni pravice"), + ("accept_and_elevate_btn_tooltip", "Sprejmi povezavo in preko nadzora uporabniškera računa povišaj pravice"), + ("clipboard_wait_response_timeout_tip", "Časovna omejitev pri kopiranju je potekla"), + ("Incoming connection", "Dohodna povezava"), + ("Outgoing connection", "Odhodna povezava"), + ("Exit", "Izhod"), + ("Open", "Odpri"), + ("logout_tip", "Ali ste prepričani, da se želite odjaviti?"), + ("Service", "Storitev"), + ("Start", "Zaženi"), + ("Stop", "Ustavi"), + ("exceed_max_devices", "Dosegli ste največje dovoljeno število upravljanih naprav."), + ("Sync with recent sessions", "Sinhroniziraj z nedavnimi sejami"), + ("Sort tags", "Uredi oznake"), + ("Open connection in new tab", "Odpri povezavo na novem zavihku"), + ("Move tab to new window", "Premakni zavihek v novo okno"), + ("Can not be empty", "Ne more biti prazno"), + ("Already exists", "Že obstaja"), + ("Change Password", "Spremeni geslo"), + ("Refresh Password", "Osveži geslo"), + ("ID", "ID"), + ("Grid View", "Mrežni pogled"), + ("List View", "Pogled seznama"), + ("Select", "Izberi"), + ("Toggle Tags", "Preklopi oznake"), + ("pull_ab_failed_tip", "Adresarja ni bilo mogoče osvežiti"), + ("push_ab_failed_tip", "Adresarja ni bilo mogoče poslati na strežnik"), + ("synced_peer_readded_tip", "Naprave, ki so bile prisotne v nedavnih sejah bodo sinhronizirane z adresarjem."), + ("Change Color", "Spremeni barvo"), + ("Primary Color", "Osnovne barve"), + ("HSV Color", "Barve HSV"), + ("Installation Successful!", "Namestitev uspešna"), + ("Installation failed!", "Namestitev ni uspela"), + ("Reverse mouse wheel", "Obrni smer drsenja miškinega kolesca"), + ("{} sessions", "{} sej"), + ("scam_title", "Lahko gre za prevaro!"), + ("scam_text1", "V primeru, da vas je nekdo, ki ga ne poznate in mu zaupate prosil, da uporabite RustDesk, prekinite klic in program zaprite."), + ("scam_text2", "RustDesk omogoča popoln nadzor nad vašim računalnikom in telefonom, in se lahko uporabi za krajo vašega denarja ali pa zasebnih podatkov."), + ("Don't show again", "Ne prikaži znova"), + ("I Agree", "Strinjam se"), + ("Decline", "Zavrni"), + ("Timeout in minutes", "Časovna omejitev v minutah"), + ("auto_disconnect_option_tip", "Samodejno prekini neaktivne seje"), + ("Connection failed due to inactivity", "Povezava je bila prekinjena zaradi neaktivnosti"), + ("Check for software update on startup", "Preveri za posodobitve ob zagonu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Prosimo, nadgradite RustDesk Server Pro na različico {} ali novejšo."), + ("pull_group_failed_tip", "Osveževanje skupine ni uspelo"), + ("Filter by intersection", "Filtriraj po preseku"), + ("Remove wallpaper during incoming sessions", "Odstrani sliko ozadja ob dohodnih povezavah"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Zaslon je bil odklopljen, preklop na primarni zaslon."), + ("No displays", "Ni zaslonov"), + ("Open in new window", "Odpri v novem oknu"), + ("Show displays as individual windows", "Prikaži zaslone kot ločena okna"), + ("Use all my displays for the remote session", "Uporabi vse zaslone za oddaljeno sejo"), + ("selinux_tip", "Na vaši napravi je omogčen SELinux, kar lahko povzroča težave pri oddaljenem nadzoru"), + ("Change view", "Spremeni pogled"), + ("Big tiles", "Velike ploščice"), + ("Small tiles", "Majhne ploščice"), + ("List", "Seznam"), + ("Virtual display", "Navidezni zaslon"), + ("Plug out all", "Odklopi vse"), + ("True color (4:4:4)", "Popolne barve (4:4:4)"), + ("Enable blocking user input", "Omogoči blokiranje vnosa"), + ("id_input_tip", "Vnesete lahko ID, neposredni IP naslov, ali pa domeno in vrata (:)\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben.\nČe želite vsiliti povezavo preko posrednika, pripnite »/r« na konec IDja, npr. »9123456234/r«."), + ("privacy_mode_impl_mag_tip", "Način 1"), + ("privacy_mode_impl_virtual_display_tip", "Način 2"), + ("Enter privacy mode", "Vstopi v zasebni način"), + ("Exit privacy mode", "Izstopi iz zasebnega načina"), + ("idd_not_support_under_win10_2004_tip", "Posredni gonilnik ni podprt. Za uporabo rabite Windows 10 2004 ali novejšo različico."), + ("input_source_1_tip", "Vir vnosa 1"), + ("input_source_2_tip", "Vir vnosa 2"), + ("Swap control-command key", "Zamenjaj tipki Ctrl-Command"), + ("swap-left-right-mouse", "Zamenjaj levo in desno tipko miške"), + ("2FA code", "Koda za dvostopenjsko preverjanje"), + ("More", "Več"), + ("enable-2fa-title", "Omogoči dvostopenjsko preverjanje"), + ("enable-2fa-desc", "Pripravite vaš TOTP avtentikator. Uporabite lahko programe kot so Authy, Microsoft ali Google Authenticator, na vašem telefonu ali računalniku.\n\nZa omogočanje dvostopenjskega preverjanja, skenirajte QR kodo in vnesite kodo, ki jo prikaže aplikacija."), + ("wrong-2fa-code", "Kode ni bilo mogoče preveriti. Preverite, da je koda pravilna, in da je nastavitev ure točna."), + ("enter-2fa-title", "Dvostopenjsko preverjanje"), + ("Email verification code must be 6 characters.", "E-poštna koda za preverjanje mora imeti 6 znakov."), + ("2FA code must be 6 digits.", "Koda za dvostopenjsko preverjanje mora imeti 6 znakov."), + ("Multiple Windows sessions found", "Najdenih je bilo več Windows sej"), + ("Please select the session you want to connect to", "Izberite sejo, v katero se želite povezati"), + ("powered_by_me", "Uporablja tehnologijo RustDesk"), + ("outgoing_only_desk_tip", "To je prilagojena različica.\nLahko se povežete na druge naprave, druge naprave pa se k vam ne morejo povezati."), + ("preset_password_warning", "Ta prilagojena različica ima prednastavljeno geslo. Kdorkoli, ki pozna to geslo, lahko prevzame popoln nadzor nad vašim računalnikom. Če tega niste pričakovali, takoj odstranite program."), + ("Security Alert", "Varnostno opozorilo"), + ("My address book", "Moj adresar"), + ("Personal", "Osebni"), + ("Owner", "Lastnik"), + ("Set shared password", "Nastavi deljeno geslo"), + ("Exist in", "Obstaja v"), + ("Read-only", "Samo za branje"), + ("Read/Write", "Branje/pisanje"), + ("Full Control", "Popoln nadzor"), + ("share_warning_tip", "Zgornja polja so deljena, in vidna vsem"), + ("Everyone", "Vsi"), + ("ab_web_console_tip", "Več na spletni konzoli"), + ("allow-only-conn-window-open-tip", "Dovoli povezavo samo če je okno RustDeska odprto"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ni fizičnih zaslonov, zasebni način ni potreben"), + ("Follow remote cursor", "Sledi oddaljenemu kazalcu"), + ("Follow remote window focus", "Sledi oddaljenemu fokusu"), + ("default_proxy_tip", "Privzeti protokol je Socks5 na vratih 1080"), + ("no_audio_input_device_tip", "Ni bilo možno najti vhodne zvočne naprave"), + ("Incoming", "Dohodno"), + ("Outgoing", "Odhodno"), + ("Clear Wayland screen selection", "Počisti izbiro Wayland zaslona"), + ("clear_Wayland_screen_selection_tip", "Po čiščenju izbire Wayland zaslona lahko ponovno izberete zaslon za delitev"), + ("confirm_clear_Wayland_screen_selection_tip", "Ali res želite počistiti izbiro Wayland zaslona?"), + ("android_new_voice_call_tip", "Prejeli ste prošnjo za nov glasovni klic. Če sprejmete, bo zvok preklopljen na glasovno komunikacijo."), + ("texture_render_tip", "Uporabi upodabljanje tekstur, za gladkejše slike. Izklopite, če imate težave pri upodabljanju."), + ("Use texture rendering", "Uporabi upodabljanje tekstur"), + ("Floating window", "Plavajoče okno"), + ("floating_window_tip", "Pomaga pri RustDesk storitvi v ozadju"), + ("Keep screen on", "Ohranite zaslon prižgan"), + ("Never", "Nikoli"), + ("During controlled", "Med nadzorom"), + ("During service is on", "Med vklopljeno storitvijo"), + ("Capture screen using DirectX", "Uporabi DirectX za zajem zaslona"), + ("Back", "Nazaj"), + ("Apps", "Aplikacije"), + ("Volume up", "Glasneje"), + ("Volume down", "Tišje"), + ("Power", "Vklop/izklop"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Če vklopite to možnost, lahko dobite kodo za dvostopenjsko preverjanje od bota. Lahko se uporabi tudi za obveščanje o povezavi."), + ("enable-bot-desc", "1. Odprite pogovor z @BotFather.\n2. Pošljite ukaz »/newbot« in prejeli boste žeton.\n3. Začnite pogovor z na novo narejenim botom. Pošljite sporočilo z desno poševnico (/) kot npr. »/hello« za aktivacijo."), + ("cancel-2fa-confirm-tip", "Ali ste prepričani, da želite ukiniti dvostopenjsko preverjanje?"), + ("cancel-bot-confirm-tip", "Ali ste prepričani, da želite ukiniti Telegram bota?"), + ("About RustDesk", "O RustDesku"), + ("Send clipboard keystrokes", "Vtipkaj vsebino odložišča"), + ("network_error_tip", "Preverite vašo mrežno povezavo, nato kliknite Ponovi."), + ("Unlock with PIN", "Odkleni s PINom"), + ("Requires at least {} characters", "Potrebuje vsaj {} znakov."), + ("Wrong PIN", "Napačen PIN"), + ("Set PIN", "Nastavi PIN"), + ("Enable trusted devices", "Omogoči zaupanja vredne naprave"), + ("Manage trusted devices", "Upravljaj zaupanja vredne naprave"), + ("Platform", "Platforma"), + ("Days remaining", "Preostane dni"), + ("enable-trusted-devices-tip", "Na zaupanja vrednih napravah ni potrebno dvostopenjsko preverjanje"), + ("Parent directory", "Nadrejena mapa"), + ("Resume", "Nadaljuj"), + ("Invalid file name", "Neveljavno ime datoteke"), + ("one-way-file-transfer-tip", "Enosmerni prenos datotek je omogočen na nadzorovani strani"), + ("Authentication Required", "Potrebno je preverjanje pristnosti"), + ("Authenticate", "Preverjanje pristnosti"), + ("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0a73e021744..7c63c8ea5ab 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Shaq cilësinë e monitorit"), ("Disable clipboard", "Ç'aktivizo clipboard"), ("Lock after session end", "Kyç pasi sesioni të përfundoj"), - ("Insert", "Fut"), + ("Insert Ctrl + Alt + Del", "Fut Ctrl + Alt + Del"), ("Insert Lock", "Fut bllokimin"), ("Refresh", "Rifresko"), ("ID does not exist", "ID nuk ekziston"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Regjistrimi"), ("Directory", "Direktoria"), ("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"), + ("Automatically record outgoing sessions", ""), ("Change", "Ndrysho"), ("Start session recording", "Fillo regjistrimin e sesionit"), ("Stop session recording", "Ndalo regjistrimin e sesionit"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 4d3654ea998..e80bb61812f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Prikaži monitor kvaliteta"), ("Disable clipboard", "Zabrani clipboard"), ("Lock after session end", "Zaključaj po završetku sesije"), - ("Insert", "Umetni"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), ("Insert Lock", "Zaključaj umetanje"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne postoji"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snimanje"), ("Directory", "Direktorijum"), ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Automatically record outgoing sessions", ""), ("Change", "Promeni"), ("Start session recording", "Započni snimanje sesije"), ("Stop session recording", "Zaustavi snimanje sesije"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9d7956545bc..dae48e7a368 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Visa bildkvalitet"), ("Disable clipboard", "Stäng av urklipp"), ("Lock after session end", "Lås efter sessionens slut"), - ("Insert", "Insert"), + ("Insert Ctrl + Alt + Del", "Insert Ctrl + Alt + Del"), ("Insert Lock", "Insert lås"), ("Refresh", "Uppdatera"), ("ID does not exist", "Detta ID existerar inte"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Spelar in"), ("Directory", "Katalog"), ("Automatically record incoming sessions", "Spela in inkommande sessioner automatiskt"), + ("Automatically record outgoing sessions", ""), ("Change", "Byt"), ("Start session recording", "Starta inspelning"), ("Stop session recording", "Avsluta inspelning"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 76e491c91ce..60b28185137 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", ""), ("Disable clipboard", ""), ("Lock after session end", ""), - ("Insert", ""), + ("Insert Ctrl + Alt + Del", ""), ("Insert Lock", ""), ("Refresh", ""), ("ID does not exist", ""), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", ""), ("Directory", ""), ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index acd14c8f9c5..71af446c144 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), - ("Insert", "แทรก"), + ("Insert Ctrl + Alt + Del", "แทรก Ctrl + Alt + Del"), ("Insert Lock", "แทรกล็อค"), ("Refresh", "รีเฟรช"), ("ID does not exist", "ไม่พอข้อมูล ID"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "การบันทึก"), ("Directory", "ไดเรกทอรี่"), ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Automatically record outgoing sessions", ""), ("Change", "เปลี่ยน"), ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), ("Stop session recording", "หยุดการบันทึกเซสซัน"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f926e94549a..ce11544b509 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Kalite monitörünü göster"), ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), ("Lock after session end", "Bağlantıdan sonra kilitle"), - ("Insert", "Ekle"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Ekle"), ("Insert Lock", "Kilit Ekle"), ("Refresh", "Yenile"), ("ID does not exist", "ID bulunamadı"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Kayıt Ediliyor"), ("Directory", "Klasör"), ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kayıt et"), + ("Automatically record outgoing sessions", ""), ("Change", "Değiştir"), ("Start session recording", "Oturum kaydını başlat"), ("Stop session recording", "Oturum kaydını sonlandır"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 9bbef24311d..b0f64f82d39 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "顯示品質監測"), ("Disable clipboard", "停用剪貼簿"), ("Lock after session end", "工作階段結束後鎖定電腦"), - ("Insert", "插入"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), ("Insert Lock", "鎖定遠端電腦"), ("Refresh", "重新載入"), ("ID does not exist", "ID 不存在"), @@ -310,7 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "開機時啟動螢幕分享服務,需要特殊權限。"), ("Connection not allowed", "不允許連線"), ("Legacy mode", "傳統模式"), - ("Map mode", "1:1 傳輸模式"), + ("Map mode", "1:1 傳輸模式"), ("Translate mode", "翻譯模式"), ("Use permanent password", "使用固定密碼"), ("Use both passwords", "同時使用兩種密碼"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "錄製"), ("Directory", "路徑"), ("Automatically record incoming sessions", "自動錄製連入的工作階段"), + ("Automatically record outgoing sessions", ""), ("Change", "變更"), ("Start session recording", "開始錄影"), ("Stop session recording", "停止錄影"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "拔出所有"), ("True color (4:4:4)", "全彩模式(4:4:4)"), ("Enable blocking user input", "允許封鎖使用者輸入"), - ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+端口號(<網域名稱>:<端口號>)。\n如果您要存取位於其他伺服器上的設備,請在ID之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), + ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+通訊埠號(<網域名稱>:<通訊埠號>)。\n如果您要存取位於其他伺服器上的設備,請在 ID 之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連接,請在 ID 的末尾添加 \"/r\",例如,\"9123456234/r\"。"), ("privacy_mode_impl_mag_tip", "模式 1"), ("privacy_mode_impl_virtual_display_tip", "模式 2"), ("Enter privacy mode", "進入隱私模式"), @@ -602,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "沒有物理螢幕,沒必要使用隱私模式。"), ("Follow remote cursor", "跟隨遠端游標"), ("Follow remote window focus", "跟隨遠端視窗焦點"), - ("default_proxy_tip", "預設代理協定及端口為 Socks5 和 1080"), + ("default_proxy_tip", "預設代理協定及通訊埠為 Socks5 和 1080"), ("no_audio_input_device_tip", "未找到音訊輸入裝置"), ("Incoming", "連入"), ("Outgoing", "連出"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "父目錄"), ("Resume", "繼續"), ("Invalid file name", "無效文件名"), + ("one-way-file-transfer-tip", "被控端啟用了單向文件傳輸"), + ("Authentication Required", "需要身分驗證"), + ("Authenticate", "認證"), + ("web_id_input_tip", "您可以輸入同一個伺服器內的 ID,Web 客戶端不支援直接 IP 存取。\n如果您要存取位於其他伺服器上的設備,請在 ID 之後添加伺服器地址(@<伺服器地址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的設備,請輸入\"@public\",不需輸入金鑰。"), + ("Download", "下載"), + ("Upload folder", "上傳資料夾"), + ("Upload files", "上傳檔案"), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6a177059cfa..ff5c8b64ae7 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Вимкнути буфер обміну"), ("Lock after session end", "Блокування після завершення сеансу"), - ("Insert", "Вставити"), + ("Insert Ctrl + Alt + Del", "Вставити Ctrl + Alt + Del"), ("Insert Lock", "Встановити замок"), ("Refresh", "Оновити"), ("ID does not exist", "ID не існує"), @@ -246,7 +246,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Paste", "Вставити"), ("Paste here?", "Вставити сюди?"), ("Are you sure to close the connection?", "Ви впевнені, що хочете завершити підключення?"), - ("Download new version", "Завантажити нову версію"), + ("Download new version", "Завантажте нову версію"), ("Touch mode", "Сенсорний режим"), ("Mouse mode", "Режим миші"), ("One-Finger Tap", "Дотик одним пальцем"), @@ -351,7 +351,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable audio", "Увімкнути аудіо"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), - ("Direct IP Access", "Прямий IP доступ"), + ("Direct IP Access", "Прямий IP-доступ"), ("Proxy", "Проксі"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відʼєднати всі прилади?"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Запис"), ("Directory", "Директорія"), ("Automatically record incoming sessions", "Автоматично записувати вхідні сеанси"), + ("Automatically record outgoing sessions", ""), ("Change", "Змінити"), ("Start session recording", "Розпочати запис сеансу"), ("Stop session recording", "Закінчити запис сеансу"), @@ -469,12 +470,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Цей файл ідентичний з тим, що на вузлі"), ("show_monitors_tip", "Показувати монітори на панелі інструментів"), ("View Mode", "Режим перегляду"), - ("login_linux_tip", "Вам необхідно залогуватися у віддалений обліковий запис Linux, щоб увімкнути стільничний сеанс X"), + ("login_linux_tip", "Вам необхідно увійти у віддалений обліковий запис Linux, щоб увімкнути стільничний сеанс X"), ("verify_rustdesk_password_tip", "Перевірте пароль RustDesk"), ("remember_account_tip", "Запамʼятати цей обліковий запис"), - ("os_account_desk_tip", "Цей обліковий запис використовується для входу до віддаленої ОС та вмикання сеансу стільниці в неграфічному режимі"), + ("os_account_desk_tip", "Цей обліковий запис використовується для входу до віддаленої ОС та вмикання сеансу стільниці в режимі без графічного інтерфейсу"), ("OS Account", "Користувач ОС"), - ("another_user_login_title_tip", "Інший користувач вже залогований"), + ("another_user_login_title_tip", "Інший користувач вже в системі"), ("another_user_login_text_tip", "Відʼєднатися"), ("xorg_not_found_title_tip", "Xorg не знайдено"), ("xorg_not_found_text_tip", "Будь ласка, встановіть Xorg"), @@ -506,7 +507,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Outgoing connection", "Вихідне підключення"), ("Exit", "Вийти"), ("Open", "Відкрити"), - ("logout_tip", "Ви впевнені, що хочете вилогуватися?"), + ("logout_tip", "Ви впевнені, що хочете вийти з системи?"), ("Service", "Служба"), ("Start", "Запустити"), ("Stop", "Зупинити"), @@ -563,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Відключити все"), ("True color (4:4:4)", "Справжній колір (4:4:4)"), ("Enable blocking user input", "Блокувати введення для користувача"), - ("id_input_tip", "Ви можете ввести ID, безпосередню IP, або ж домен з портом (<домен>:<порт>).\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", ключ для публічного сервера не потрібен."), + ("id_input_tip", "Ви можете ввести ID, безпосередню IP, або ж домен з портом (<домен>:<порт>).\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), ("privacy_mode_impl_mag_tip", "Режим 1"), ("privacy_mode_impl_virtual_display_tip", "Режим 2"), ("Enter privacy mode", "Увійти в режим конфіденційності"), @@ -631,18 +632,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-bot-confirm-tip", "Ви впевнені, що хочете скасувати Telegram бота?"), ("About RustDesk", "Про Rustdesk"), ("Send clipboard keystrokes", "Надіслати вміст буфера обміну"), - ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисність \"Повторити\""), + ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисніть \"Повторити\""), ("Unlock with PIN", "Розблокування PIN-кодом"), ("Requires at least {} characters", "Потрібно щонайменше {} символів"), ("Wrong PIN", "Неправильний PIN-код"), ("Set PIN", "Встановити PIN-код"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Enable trusted devices", "Увімкнути довірені пристрої"), + ("Manage trusted devices", "Керувати довіреними пристроями"), + ("Platform", "Платформа"), + ("Days remaining", "Залишилося днів"), + ("enable-trusted-devices-tip", "Пропускати двофакторну автентифікацію на довірених пристроях"), + ("Parent directory", "Батьківський каталог"), + ("Resume", "Продовжити"), + ("Invalid file name", "Неправильна назва файлу"), + ("one-way-file-transfer-tip", "На стороні, що керується, увімкнено односторонню передачу файлів."), + ("Authentication Required", "Потрібна автентифікація"), + ("Authenticate", "Автентифікувати"), + ("web_id_input_tip", "Ви можете ввести ID з того самого серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 88f70a8e2c4..5a2c47befcc 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -130,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Hiện thị chất lượng của màn hình"), ("Disable clipboard", "Tắt clipboard"), ("Lock after session end", "Khóa sau khi kết thúc phiên kết nối"), - ("Insert", "Cài"), + ("Insert Ctrl + Alt + Del", "Cài Ctrl + Alt + Del"), ("Insert Lock", "Cài khóa"), ("Refresh", "Làm mới"), ("ID does not exist", "ID không tồn tại"), @@ -364,6 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Đang ghi hình"), ("Directory", "Thư mục"), ("Automatically record incoming sessions", "Tự động ghi những phiên kết nối vào"), + ("Automatically record outgoing sessions", ""), ("Change", "Thay đổi"), ("Start session recording", "Bắt đầu ghi hình phiên kết nối"), ("Stop session recording", "Dừng ghi hình phiên kết nối"), @@ -644,5 +645,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", ""), ("Resume", ""), ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index f8d917a518e..7f9ca4e9aaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ mod keyboard; -#[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -12,7 +11,6 @@ mod server; #[cfg(not(any(target_os = "ios")))] pub use self::server::*; mod client; -#[cfg(not(any(target_os = "ios")))] mod lan; #[cfg(not(any(target_os = "ios")))] mod rendezvous_mediator; diff --git a/src/main.rs b/src/main.rs index bc41365e3fb..f295363aa90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,6 @@ fn main() { } common::test_rendezvous_server(); common::test_nat_type(); - #[cfg(target_os = "android")] - crate::common::check_software_update(); common::global_clean(); } @@ -102,7 +100,7 @@ fn main() { cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { log::info!("id={}", hbb_common::config::Config::get_id()); - crate::start_server(true); + crate::start_server(true, false); } common::global_clean(); } diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs new file mode 100644 index 00000000000..9aeea1e2b01 --- /dev/null +++ b/src/platform/gtk_sudo.rs @@ -0,0 +1,771 @@ +// https://github.com/aarnt/qt-sudo +// Sometimes reboot is needed to refresh sudoers. + +use crate::lang::translate; +use gtk::{glib, prelude::*}; +use hbb_common::{ + anyhow::{bail, Error}, + log, ResultType, +}; +use nix::{ + libc::{fcntl, kill}, + pty::{forkpty, ForkptyResult}, + sys::{ + signal::Signal, + wait::{waitpid, WaitPidFlag}, + }, + unistd::{execvp, setsid, Pid}, +}; +use std::{ + ffi::CString, + fs::File, + io::{Read, Write}, + os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, +}; + +const EXIT_CODE: i32 = -1; + +enum Message { + PasswordPrompt((String, bool)), + Password((String, String)), + ErrorDialog(String), + Cancel, + Exit(i32), +} + +pub fn run(cmds: Vec<&str>) -> ResultType<()> { + // rustdesk service kill `rustdesk --` processes + let second_arg = std::env::args().nth(1).unwrap_or_default(); + let cmd_mode = + second_arg.starts_with("--") && second_arg != "--tray" && second_arg != "--no-server"; + let mod_arg = if cmd_mode { "cmd" } else { "gui" }; + let mut args = vec!["-gtk-sudo", mod_arg]; + args.append(&mut cmds.clone()); + let mut child = crate::run_me(args)?; + let exit_status = child.wait()?; + if exit_status.success() { + Ok(()) + } else { + bail!("child exited with status: {:?}", exit_status); + } +} + +pub fn exec() { + let mut args = vec![]; + for arg in std::env::args().skip(3) { + args.push(arg); + } + let cmd_mode = std::env::args().nth(2) == Some("cmd".to_string()); + if cmd_mode { + cmd(args); + } else { + ui(args); + } +} + +fn cmd(args: Vec) { + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = cmd_parent(child, master) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(None, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + std::process::exit(EXIT_CODE); + } + } +} + +fn ui(args: Vec) { + // https://docs.gtk.org/gtk4/ctor.Application.new.html + // https://docs.gtk.org/gio/type_func.Application.id_is_valid.html + let application = gtk::Application::new(None, Default::default()); + + let (tx_to_ui, rx_to_ui) = channel::(); + let (tx_from_ui, rx_from_ui) = channel::(); + + let rx_to_ui = Arc::new(Mutex::new(rx_to_ui)); + let tx_from_ui = Arc::new(Mutex::new(tx_from_ui)); + + let rx_to_ui_clone = rx_to_ui.clone(); + let tx_from_ui_clone = tx_from_ui.clone(); + + let username = Arc::new(Mutex::new(crate::platform::get_active_username())); + let username_clone = username.clone(); + + application.connect_activate(glib::clone!(@weak application =>move |_| { + let rx_to_ui = rx_to_ui_clone.clone(); + let tx_from_ui = tx_from_ui_clone.clone(); + let last_password = Arc::new(Mutex::new(String::new())); + let username = username_clone.clone(); + + glib::timeout_add_local(std::time::Duration::from_millis(50), move || { + if let Ok(msg) = rx_to_ui.lock().unwrap().try_recv() { + match msg { + Message::PasswordPrompt((err_msg, show_edit)) => { + let last_pwd = last_password.lock().unwrap().clone(); + let username = username.lock().unwrap().clone(); + if let Some((username, password)) = password_prompt(&username, &last_pwd, &err_msg, show_edit) { + *last_password.lock().unwrap() = password.clone(); + if let Err(e) = tx_from_ui + .lock() + .unwrap() + .send(Message::Password((username, password))) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } else { + if let Err(e) = tx_from_ui.lock().unwrap().send(Message::Cancel) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } + } + Message::ErrorDialog(err_msg) => { + error_dialog_and_exit(&err_msg, EXIT_CODE); + } + Message::Exit(code) => { + log::info!("Exit code: {}", code); + std::process::exit(code); + } + _ => {} + } + } + glib::ControlFlow::Continue + }); + })); + + let tx_to_ui_clone = tx_to_ui.clone(); + std::thread::spawn(move || { + let acitve_user = crate::platform::get_active_username(); + let mut initial_password = None; + if acitve_user != "root" { + if let Err(e) = tx_to_ui_clone.send(Message::PasswordPrompt(("".to_string(), true))) { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + match rx_from_ui.recv() { + Ok(Message::Password((user, password))) => { + *username.lock().unwrap() = user; + initial_password = Some(password); + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + std::process::exit(EXIT_CODE); + } + _ => { + log::error!("Unexpected message"); + std::process::exit(EXIT_CODE); + } + } + } + let username = username.lock().unwrap().clone(); + let su_user = if username == acitve_user { + None + } else { + Some(username) + }; + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = ui_parent( + child, + master, + tx_to_ui_clone, + rx_from_ui, + su_user.is_some(), + initial_password, + ) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(su_user, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + if let Err(e) = + tx_to_ui.send(Message::ErrorDialog(format!("Forkpty error: {:?}", err))) + { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + } + } + }); + + let _holder = application.hold(); + let args: Vec<&str> = vec![]; + application.run_with_args(&args); + log::debug!("exit from gtk::Application::run_with_args"); + std::process::exit(EXIT_CODE); +} + +fn cmd_parent(child: Pid, master: OwnedFd) -> ResultType<()> { + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + let mut stdout = std::io::stdout(); + let stdin = std::io::stdin(); + let stdin_fd = stdin.as_raw_fd(); + let old_termios = termios::Termios::from_fd(stdin_fd)?; + turn_off_echo(stdin_fd).ok(); + shutdown_hooks::add_shutdown_hook(turn_on_echo_shutdown_hook); + let (tx, rx) = channel::>(); + std::thread::spawn(move || loop { + let mut line = String::default(); + match stdin.read_line(&mut line) { + Ok(0) => { + kill_child(child); + break; + } + Ok(_) => { + if let Err(e) = tx.send(line.as_bytes().to_vec()) { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} + Err(e) => { + log::info!("Failed to read stdin: {e:?}"); + kill_child(child); + break; + } + }; + }); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + let buf = String::from_utf8_lossy(&buf[..n]).to_string(); + print!("{}", buf); + if let Err(e) = stdout.flush() { + log::error!("flush failed: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::info!("Read child error: {:?}", e); + break; + } + } + match rx.try_recv() { + Ok(v) => { + if let Err(e) = file.write_all(&v) { + log::error!("write error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => {} + std::sync::mpsc::TryRecvError::Disconnected => { + log::error!("receive error: {e:?}"); + kill_child(child); + break; + } + }, + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + termios::tcsetattr(stdin_fd, termios::TCSANOW, &old_termios).ok(); + std::process::exit(code); +} + +fn ui_parent( + child: Pid, + master: OwnedFd, + tx_to_ui: Sender, + rx_from_ui: Receiver, + is_su: bool, + initial_password: Option, +) -> ResultType<()> { + let mut initial_password = initial_password; + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + tx_to_ui.send(Message::ErrorDialog(format!("fcntl error: {errno:?}")))?; + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + + let mut first = initial_password.is_none(); + let mut su_password_sent = false; + let mut saved_output = String::default(); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + saved_output = String::default(); + let buf = String::from_utf8_lossy(&buf[..n]).trim().to_string(); + let last_line = buf.lines().last().unwrap_or(&buf).trim().to_string(); + log::info!("read from child: {}", buf); + + if last_line.starts_with("sudo:") || last_line.starts_with("su:") { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(last_line)) { + log::error!("Channel error: {e:?}"); + kill_child(child); + } + break; + } else if last_line.ends_with(":") { + match get_echo_turn_off(raw_fd) { + Ok(true) => { + log::debug!("get_echo_turn_off ok"); + if let Some(password) = initial_password.clone() { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + if is_su && !su_password_sent { + su_password_sent = true; + continue; + } + initial_password = None; + continue; + } + // In fact, su mode can only input password once + let err_msg = if first { "" } else { "Sorry, try again." }; + first = false; + if let Err(e) = + tx_to_ui.send(Message::PasswordPrompt((err_msg.to_string(), false))) + { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + match rx_from_ui.recv() { + Ok(Message::Password((_, password))) => { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + kill_child(child); + break; + } + _ => { + log::error!("Unexpected message"); + break; + } + } + } + Ok(false) => log::warn!("get_echo_turn_off timeout"), + Err(e) => log::error!("get_echo_turn_off error: {:?}", e), + } + } else { + saved_output = buf.clone(); + if !last_line.is_empty() && initial_password.is_some() { + log::error!("received not empty line: {last_line}, clear initial password"); + initial_password = None; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::debug!("Read error: {:?}", e); + break; + } + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + + if code != 0 && !saved_output.is_empty() { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(saved_output.clone())) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + return Ok(()); + } + if let Err(e) = tx_to_ui.send(Message::Exit(code)) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + Ok(()) +} + +fn child(su_user: Option, args: Vec) -> ResultType<()> { + // https://doc.rust-lang.org/std/env/consts/constant.OS.html + let os = std::env::consts::OS; + let bsd = os == "freebsd" || os == "dragonfly" || os == "netbsd" || os == "openbad"; + let mut params = vec!["sudo".to_string()]; + if su_user.is_some() { + params.push("-S".to_string()); + } + params.push("/bin/sh".to_string()); + params.push("-c".to_string()); + + let command = args + .iter() + .map(|s| { + if su_user.is_some() { + s.to_string() + } else { + quote_shell_arg(s, true) + } + }) + .collect::>() + .join(" "); + let mut command = if bsd { + let lc = match std::env::var("LC_ALL") { + Ok(lc_all) => { + if lc_all.contains('\'') { + eprintln!( + "sudo: Detected attempt to inject privileged command via LC_ALL env({lc_all}). Exiting!\n", + ); + std::process::exit(EXIT_CODE); + } + format!("LC_ALL='{lc_all}' ") + } + Err(_) => { + format!("unset LC_ALL;") + } + }; + format!("{}exec {}", lc, command) + } else { + command.to_string() + }; + if su_user.is_some() { + command = format!("'{}'", quote_shell_arg(&command, false)); + } + params.push(command); + std::env::set_var("LC_ALL", "C"); + + if let Some(user) = &su_user { + let su_subcommand = params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(" "); + params = vec![ + "su".to_string(), + "-".to_string(), + user.to_string(), + "-c".to_string(), + su_subcommand, + ]; + } + + // allow failure here + let _ = setsid(); + let mut cparams = vec![]; + for param in ¶ms { + cparams.push(CString::new(param.as_str())?); + } + let su_or_sudo = if su_user.is_some() { "su" } else { "sudo" }; + let res = execvp(CString::new(su_or_sudo)?.as_c_str(), &cparams); + eprintln!("sudo: execvp error: {:?}", res); + std::process::exit(EXIT_CODE); +} + +fn get_echo_turn_off(fd: RawFd) -> Result { + let tios = termios::Termios::from_fd(fd)?; + for _ in 0..10 { + if tios.c_lflag & termios::ECHO == 0 { + return Ok(true); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Ok(false) +} + +fn turn_off_echo(fd: RawFd) -> Result<(), Error> { + use termios::*; + let mut termios = Termios::from_fd(fd)?; + // termios.c_lflag &= !(ECHO | ECHONL | ICANON | IEXTEN); + termios.c_lflag &= !ECHO; + tcsetattr(fd, TCSANOW, &termios)?; + Ok(()) +} + +pub extern "C" fn turn_on_echo_shutdown_hook() { + let fd = std::io::stdin().as_raw_fd(); + if let Ok(mut termios) = termios::Termios::from_fd(fd) { + termios.c_lflag |= termios::ECHO; + termios::tcsetattr(fd, termios::TCSANOW, &termios).ok(); + } +} + +fn kill_child(child: Pid) { + unsafe { kill(child.as_raw(), Signal::SIGINT as _) }; + let mut res = 0; + + for _ in 0..10 { + match waitpid(child, Some(WaitPidFlag::WNOHANG)) { + Ok(_) => { + res = 1; + break; + } + Err(_) => (), + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + if res == 0 { + log::info!("Force killing child process"); + unsafe { kill(child.as_raw(), Signal::SIGKILL as _) }; + } +} + +fn password_prompt( + username: &str, + last_password: &str, + err: &str, + show_edit: bool, +) -> Option<(String, String)> { + let dialog = gtk::Dialog::builder() + .title(crate::get_app_name()) + .modal(true) + .build(); + // https://docs.gtk.org/gtk4/method.Dialog.set_default_response.html + dialog.set_default_response(gtk::ResponseType::Ok); + let content_area = dialog.content_area(); + + let label = gtk::Label::builder() + .label(translate("Authentication Required".to_string())) + .margin_top(10) + .build(); + content_area.add(&label); + + let image = gtk::Image::from_icon_name(Some("avatar-default-symbolic"), gtk::IconSize::Dialog); + image.set_margin_top(10); + content_area.add(&image); + + let user_label = gtk::Label::new(Some(username)); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + let edit_icon = + gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button.into()); + edit_button.set_image(Some(&edit_icon)); + edit_button.set_can_focus(false); + let user_entry = gtk::Entry::new(); + user_entry.set_alignment(0.5); + user_entry.set_width_request(100); + let user_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + user_box.add(&user_label); + user_box.add(&edit_button); + user_box.add(&user_entry); + user_box.set_halign(gtk::Align::Center); + user_box.set_valign(gtk::Align::Center); + user_box.set_vexpand(true); + content_area.add(&user_box); + + edit_button.connect_clicked( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry=> move |_| { + let username = user_label.text().to_string(); + user_entry.set_text(&username); + user_label.hide(); + edit_button.hide(); + user_entry.show(); + user_entry.grab_focus(); + }), + ); + + let password_input = gtk::Entry::builder() + .visibility(false) + .input_purpose(gtk::InputPurpose::Password) + .placeholder_text(translate("Password".to_string())) + .margin_top(20) + .margin_start(30) + .margin_end(30) + .activates_default(true) + .text(last_password) + .build(); + password_input.set_alignment(0.5); + // https://docs.gtk.org/gtk3/signal.Entry.activate.html + password_input.connect_activate(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + content_area.add(&password_input); + + user_entry.connect_focus_out_event( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => @default-return glib::Propagation::Proceed, move |_, _| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + glib::Propagation::Proceed + }), + ); + user_entry.connect_activate( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => move |_| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + password_input.grab_focus(); + }), + ); + + if !err.is_empty() { + let err_label = gtk::Label::new(None); + err_label.set_markup(&format!( + "{}", + err + )); + err_label.set_selectable(true); + content_area.add(&err_label); + } + + let cancel_button = gtk::Button::builder() + .label(translate("Cancel".to_string())) + .hexpand(true) + .build(); + cancel_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Cancel); + })); + let authenticate_button = gtk::Button::builder() + .label(translate("Authenticate".to_string())) + .hexpand(true) + .build(); + authenticate_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .hexpand(true) + .homogeneous(true) + .spacing(10) + .margin_top(10) + .build(); + button_box.add(&cancel_button); + button_box.add(&authenticate_button); + content_area.add(&button_box); + + content_area.set_spacing(10); + content_area.set_border_width(10); + + dialog.set_width_request(400); + dialog.show_all(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + password_input.grab_focus(); + user_entry.hide(); + if !show_edit { + edit_button.hide(); + } + dialog.check_resize(); + let response = dialog.run(); + dialog.hide(); + + if response == gtk::ResponseType::Ok { + let username = if user_entry.get_visible() { + user_entry.text().to_string() + } else { + user_label.text().to_string() + }; + Some((username, password_input.text().to_string())) + } else { + None + } +} + +fn error_dialog_and_exit(err_msg: &str, exit_code: i32) { + log::error!("Error dialog: {err_msg}, exit code: {exit_code}"); + let dialog = gtk::MessageDialog::builder() + .message_type(gtk::MessageType::Error) + .title(crate::get_app_name()) + .text("Error") + .secondary_text(err_msg) + .modal(true) + .buttons(gtk::ButtonsType::Ok) + .build(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + dialog.run(); + dialog.close(); + std::process::exit(exit_code); +} + +fn quote_shell_arg(arg: &str, add_splash_if_match: bool) -> String { + let mut rv = arg.to_string(); + let re = hbb_common::regex::Regex::new("(\\s|[][!\"#$&'()*,;<=>?\\^`{}|~])"); + let Ok(re) = re else { + return rv; + }; + if re.is_match(arg) { + rv = rv.replace("'", "'\\''"); + if add_splash_if_match { + rv = format!("'{}'", rv); + } + } + rv +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 90e2f52ca09..08cf0fb9a90 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,4 +1,4 @@ -use super::{CursorData, ResultType}; +use super::{gtk_sudo, CursorData, ResultType}; use desktop::Desktop; use hbb_common::config::keys::OPTION_ALLOW_LINUX_HEADLESS; pub use hbb_common::platform::linux::*; @@ -15,8 +15,6 @@ use hbb_common::{ use std::{ cell::RefCell, ffi::OsStr, - fs::File, - io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::{Child, Command}, string::String, @@ -612,8 +610,15 @@ pub fn get_env_var(k: &str) -> String { } } +fn is_flatpak() -> bool { + std::path::PathBuf::from("/.flatpak-info").exists() +} + // Headless is enabled, always return true. pub fn is_prelogin() -> bool { + if is_flatpak() { + return false; + } let n = get_active_userid().len(); n < 4 && n > 1 } @@ -766,30 +771,18 @@ pub fn quit_gui() { unsafe { gtk_main_quit() }; } +/* pub fn exec_privileged(args: &[&str]) -> ResultType { Ok(Command::new("pkexec").args(args).spawn()?) } +*/ pub fn check_super_user_permission() -> ResultType { - let file = format!( - "/usr/share/{}/files/polkit", - crate::get_app_name().to_lowercase() - ); - let arg; - if Path::new(&file).is_file() { - arg = file.as_str(); - } else { - arg = "echo"; - } - // https://github.com/rustdesk/rustdesk/issues/2756 - if let Ok(status) = Command::new("pkexec").arg(arg).status() { - // https://github.com/rustdesk/rustdesk/issues/5205#issuecomment-1658059657s - Ok(status.code() != Some(126) && status.code() != Some(127)) - } else { - Ok(true) - } + gtk_sudo::run(vec!["echo"])?; + Ok(true) } +/* pub fn elevate(args: Vec<&str>) -> ResultType { let cmd = std::env::current_exe()?; match cmd.to_str() { @@ -824,6 +817,7 @@ pub fn elevate(args: Vec<&str>) -> ResultType { } } } +*/ type GtkSettingsPtr = *mut c_void; type GObjectPtr = *mut c_void; @@ -1324,21 +1318,8 @@ fn has_cmd(cmd: &str) -> bool { .unwrap_or_default() } -pub fn run_cmds_pkexec(cmds: &str) -> bool { - const DONE: &str = "RUN_CMDS_PKEXEC_DONE"; - if let Ok(output) = std::process::Command::new("pkexec") - .arg("sh") - .arg("-c") - .arg(&format!("{cmds} echo {DONE}")) - .output() - { - let out = String::from_utf8_lossy(&output.stdout); - log::debug!("cmds: {cmds}"); - log::debug!("output: {out}"); - out.contains(DONE) - } else { - false - } +pub fn run_cmds_privileged(cmds: &str) -> bool { + crate::platform::gtk_sudo::run(vec![cmds]).is_ok() } pub fn run_me_with(secs: u32) { @@ -1367,17 +1348,20 @@ fn switch_service(stop: bool) -> String { pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { if !has_cmd("systemctl") { + // Failed when installed + flutter run + started by `show_new_window`. return false; } log::info!("Uninstalling service..."); let cp = switch_service(true); let app_name = crate::get_app_name().to_lowercase(); - if !run_cmds_pkexec(&format!( - "systemctl disable {app_name}; systemctl stop {app_name}; {cp}" + // systemctl kill rustdesk --tray, execute cp first + if !run_cmds_privileged(&format!( + "{cp} systemctl disable {app_name}; systemctl stop {app_name};" )) { Config::set_option("stop-service".into(), "".into()); return true; } + // systemctl stop will kill child processes, below may not be executed. if show_new_window { run_me_with(2); } @@ -1392,8 +1376,8 @@ pub fn install_service() -> bool { log::info!("Installing service..."); let cp = switch_service(false); let app_name = crate::get_app_name().to_lowercase(); - if !run_cmds_pkexec(&format!( - "{cp} systemctl enable {app_name}; systemctl stop {app_name}; systemctl start {app_name};" + if !run_cmds_privileged(&format!( + "{cp} systemctl enable {app_name}; systemctl start {app_name};" )) { Config::set_option("stop-service".into(), "Y".into()); } @@ -1403,9 +1387,9 @@ pub fn install_service() -> bool { fn check_if_stop_service() { if Config::get_option("stop-service".into()) == "Y" { let app_name = crate::get_app_name().to_lowercase(); - allow_err!(run_cmds( + allow_err!(run_cmds(&format!( "systemctl disable {app_name}; systemctl stop {app_name}" - )); + ))); } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3d14485d269..b3c5546a6fc 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -24,6 +24,7 @@ use hbb_common::{ sysinfo::{Pid, Process, ProcessRefreshKind, System}, }; use include_dir::{include_dir, Dir}; +use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; use std::path::PathBuf; @@ -60,6 +61,10 @@ pub fn major_version() -> u32 { } pub fn is_process_trusted(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_process_trusted(prompt)) +} + +fn unsafe_is_process_trusted(prompt: bool) -> bool { unsafe { let value = if prompt { YES } else { NO }; let value: id = msg_send![class!(NSNumber), numberWithBool: value]; @@ -79,10 +84,14 @@ pub fn is_can_input_monitoring(prompt: bool) -> bool { } } +pub fn is_can_screen_recording(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_can_screen_recording(prompt)) +} + // macOS >= 10.15 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk -pub fn is_can_screen_recording(prompt: bool) -> bool { +fn unsafe_is_can_screen_recording(prompt: bool) -> bool { // we got some report that we show no permission even after set it, so we try to use new api for screen recording check // the new api is only available on macOS >= 10.15, but on stackoverflow, some people said it works on >= 10.16 (crash on 10.15), // but also some said it has bug on 10.16, so we just use it on 11.0. @@ -297,6 +306,10 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { } pub fn get_focused_display(displays: Vec) -> Option { + autoreleasepool(|| unsafe_get_focused_display(displays)) +} + +fn unsafe_get_focused_display(displays: Vec) -> Option { unsafe { let main_screen: id = msg_send![class!(NSScreen), mainScreen]; let screen: id = msg_send![main_screen, deviceDescription]; @@ -311,6 +324,10 @@ pub fn get_focused_display(displays: Vec) -> Option { } pub fn get_cursor() -> ResultType> { + autoreleasepool(|| unsafe_get_cursor()) +} + +fn unsafe_get_cursor() -> ResultType> { unsafe { let seed = CGSCurrentCursorSeed(); if seed == LATEST_SEED { @@ -375,8 +392,12 @@ fn get_cursor_id() -> ResultType<(id, u64)> { } } -// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c pub fn get_cursor_data(hcursor: u64) -> ResultType { + autoreleasepool(|| unsafe_get_cursor_data(hcursor)) +} + +// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c +fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { unsafe { let (c, hcursor2) = get_cursor_id()?; if hcursor != hcursor2 { @@ -519,9 +540,9 @@ pub fn start_os_service() { ); std::process::exit(-1); }; - if my_start_time <= start_time + 1 { + if my_start_time <= start_time + 3 { log::error!( - "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work", + "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work, earlier more 3 seconds", ); std::process::exit(-1); } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fe66e50dc69..d0ddd09bf70 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -23,6 +23,9 @@ pub mod linux; #[cfg(target_os = "linux")] pub mod linux_desktop_manager; +#[cfg(target_os = "linux")] +pub mod gtk_sudo; + #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::{message_proto::CursorData, ResultType}; use std::sync::{Arc, Mutex}; @@ -100,6 +103,7 @@ impl WakeLock { } } +#[cfg(not(target_os = "ios"))] pub fn get_wakelock(_display: bool) -> WakeLock { hbb_common::log::info!("new wakelock, require display on: {_display}"); #[cfg(target_os = "android")] @@ -127,6 +131,12 @@ impl Drop for InstallingService { } } +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +pub fn is_prelogin() -> bool { + false +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 7ed76d2e4be..9ee3c1f5c9a 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -549,7 +549,9 @@ extern "C" continue; if (!stricmp(info.pWinStationName, "console")) { - return info.SessionId; + auto id = info.SessionId; + WTSFreeMemory(pInfos); + return id; } if (!strnicmp(info.pWinStationName, rdp, nrdp)) { diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index de85e7ba8dc..782d7ed75a8 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -34,7 +34,7 @@ const CONFIG_KEY_REG_RECOVERY: &str = "reg_recovery"; struct Display { dm: DEVMODEW, name: [WCHAR; 32], - _primary: bool, + primary: bool, } pub struct PrivacyModeImpl { @@ -135,7 +135,7 @@ impl PrivacyModeImpl { let display = Display { dm, name: dd.DeviceName, - _primary: primary, + primary, }; let ds = virtual_display_manager::get_cur_device_string(); @@ -150,8 +150,11 @@ impl PrivacyModeImpl { } fn restore_plug_out_monitor(&mut self) { - let _ = - virtual_display_manager::plug_out_monitor_indices(&self.virtual_displays_added, true); + let _ = virtual_display_manager::plug_out_monitor_indices( + &self.virtual_displays_added, + true, + false, + ); self.virtual_displays_added.clear(); } @@ -312,7 +315,7 @@ impl PrivacyModeImpl { // No physical displays, no need to use the privacy mode. if self.displays.is_empty() { - virtual_display_manager::plug_out_monitor_indices(&displays, false)?; + virtual_display_manager::plug_out_monitor_indices(&displays, false, false)?; bail!(NO_PHYSICAL_DISPLAYS); } @@ -357,6 +360,35 @@ impl PrivacyModeImpl { } Ok(()) } + + fn restore(&mut self) { + Self::restore_displays(&self.displays); + Self::restore_displays(&self.virtual_displays); + allow_err!(Self::commit_change_display(0)); + self.restore_plug_out_monitor(); + self.displays.clear(); + self.virtual_displays.clear(); + } + + fn restore_displays(displays: &[Display]) { + for display in displays { + unsafe { + let mut dm = display.dm.clone(); + let flags = if display.primary { + CDS_NORESET | CDS_UPDATEREGISTRY | CDS_SET_PRIMARY + } else { + CDS_NORESET | CDS_UPDATEREGISTRY + }; + ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut dm, + std::ptr::null_mut(), + flags, + std::ptr::null_mut(), + ); + } + } + } } impl PrivacyMode for PrivacyModeImpl { @@ -431,14 +463,9 @@ impl PrivacyMode for PrivacyModeImpl { ) -> ResultType<()> { self.check_off_conn_id(conn_id)?; super::win_input::unhook()?; - let virtual_display_added = self.virtual_displays_added.len() > 0; - if virtual_display_added { - self.restore_plug_out_monitor(); - } + let _tmp_ignore_changed_holder = crate::display_service::temp_ignore_displays_changed(); + self.restore(); restore_reg_connectivity(false); - if !virtual_display_added { - Self::commit_change_display(CDS_RESET)?; - } if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { if let Some(state) = state { @@ -485,7 +512,7 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { return; } if plug_out_monitors { - let _ = virtual_display_manager::plug_out_monitor(-1, true); + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); } if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 4ae222966e9..f5d81eaffa6 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -12,10 +12,7 @@ use uuid::Uuid; use hbb_common::{ allow_err, anyhow::{self, bail}, - config::{ - self, keys::*, option2bool, Config, CONNECT_TIMEOUT, READ_TIMEOUT, REG_INTERVAL, - RENDEZVOUS_PORT, - }, + config::{self, keys::*, option2bool, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT}, futures::future::join_all, log, protobuf::Message as _, @@ -79,8 +76,11 @@ impl RendezvousMediator { tokio::spawn(async move { direct_server(server_cloned).await; }); + #[cfg(target_os = "android")] + let start_lan_listening = true; #[cfg(not(any(target_os = "android", target_os = "ios")))] - if crate::platform::is_installed() { + let start_lan_listening = crate::platform::is_installed(); + if start_lan_listening { std::thread::spawn(move || { allow_err!(super::lan::start_listening()); }); @@ -703,123 +703,6 @@ async fn direct_server(server: ServerPtr) { } } -pub async fn query_online_states, Vec)>(ids: Vec, f: F) { - let test = false; - if test { - sleep(1.5).await; - let mut onlines = ids; - let offlines = onlines.drain((onlines.len() / 2)..).collect(); - f(onlines, offlines) - } else { - let query_begin = Instant::now(); - let query_timeout = std::time::Duration::from_millis(3_000); - loop { - if SHOULD_EXIT.load(Ordering::SeqCst) { - break; - } - match query_online_states_(&ids, query_timeout).await { - Ok((onlines, offlines)) => { - f(onlines, offlines); - break; - } - Err(e) => { - log::debug!("{}", &e); - } - } - - if query_begin.elapsed() > query_timeout { - log::debug!( - "query onlines timeout {:?} ({:?})", - query_begin.elapsed(), - query_timeout - ); - break; - } - - sleep(1.5).await; - } - } -} - -async fn create_online_stream() -> ResultType { - let (rendezvous_server, _servers, _contained) = - crate::get_rendezvous_server(READ_TIMEOUT).await; - let tmp: Vec<&str> = rendezvous_server.split(":").collect(); - if tmp.len() != 2 { - bail!("Invalid server address: {}", rendezvous_server); - } - let port: u16 = tmp[1].parse()?; - if port == 0 { - bail!("Invalid server address: {}", rendezvous_server); - } - let online_server = format!("{}:{}", tmp[0], port - 1); - connect_tcp(online_server, CONNECT_TIMEOUT).await -} - -async fn query_online_states_( - ids: &Vec, - timeout: std::time::Duration, -) -> ResultType<(Vec, Vec)> { - let query_begin = Instant::now(); - - let mut msg_out = RendezvousMessage::new(); - msg_out.set_online_request(OnlineRequest { - id: Config::get_id(), - peers: ids.clone(), - ..Default::default() - }); - - loop { - if SHOULD_EXIT.load(Ordering::SeqCst) { - // No need to care about onlines - return Ok((Vec::new(), Vec::new())); - } - - let mut socket = match create_online_stream().await { - Ok(s) => s, - Err(e) => { - log::debug!("Failed to create peers online stream, {e}"); - return Ok((vec![], ids.clone())); - } - }; - if let Err(e) = socket.send(&msg_out).await { - log::debug!("Failed to send peers online states query, {e}"); - return Ok((vec![], ids.clone())); - } - if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg(&mut socket, None).await { - match msg_in.union { - Some(rendezvous_message::Union::OnlineResponse(online_response)) => { - let states = online_response.states; - let mut onlines = Vec::new(); - let mut offlines = Vec::new(); - for i in 0..ids.len() { - // bytes index from left to right - let bit_value = 0x01 << (7 - i % 8); - if (states[i / 8] & bit_value) == bit_value { - onlines.push(ids[i].clone()); - } else { - offlines.push(ids[i].clone()); - } - } - return Ok((onlines, offlines)); - } - _ => { - // ignore - } - } - } else { - // TODO: Make sure socket closed? - bail!("Online stream receives None"); - } - - if query_begin.elapsed() > timeout { - bail!("Try query onlines timeout {:?}", &timeout); - } - - sleep(300.0).await; - } -} - enum Sink<'a> { Framed(&'a mut FramedSocket, &'a TargetAddr<'a>), Stream(&'a mut FramedStream), @@ -833,24 +716,3 @@ impl Sink<'_> { } } } - -#[cfg(test)] -mod tests { - use hbb_common::tokio; - - #[tokio::test] - async fn test_query_onlines() { - super::query_online_states( - vec![ - "152183996".to_owned(), - "165782066".to_owned(), - "155323351".to_owned(), - "460952777".to_owned(), - ], - |onlines: Vec, offlines: Vec| { - println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); - }, - ) - .await; - } -} diff --git a/src/server.rs b/src/server.rs index 547886a5c98..02522db9684 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,7 +106,11 @@ pub fn new() -> ServerPtr { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); - server.add_service(Box::new(input_service::new_window_focus())); + #[cfg(target_os = "linux")] + if scrap::is_x11() { + // wayland does not support multiple displays currently + server.add_service(Box::new(input_service::new_window_focus())); + } } } Arc::new(RwLock::new(server)) @@ -456,16 +460,21 @@ pub async fn start_server(_is_server: bool) { /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. +/// * `no_server` - If `is_server` is false, whether to start a server if not found. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main] -pub async fn start_server(is_server: bool) { - #[cfg(target_os = "linux")] - { - log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); - log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); - } - #[cfg(windows)] - hbb_common::platform::windows::start_cpu_performance_monitor(); +pub async fn start_server(is_server: bool, no_server: bool) { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + #[cfg(target_os = "linux")] + { + log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); + log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); + } + #[cfg(windows)] + hbb_common::platform::windows::start_cpu_performance_monitor(); + }); if is_server { crate::common::set_server_running(true); @@ -516,8 +525,14 @@ pub async fn start_server(is_server: bool) { crate::ipc::client_get_hwcodec_config_thread(0); } Err(err) => { - log::info!("server not started (will try to start): {}", err); - std::thread::spawn(|| start_server(true)); + log::info!("server not started: {err:?}, no_server: {no_server}"); + if no_server { + hbb_common::sleep(1.0).await; + std::thread::spawn(|| start_server(false, true)); + } else { + log::info!("try start server"); + std::thread::spawn(|| start_server(true, false)); + } } } } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 3040a8f88f3..3aadb3ad5dd 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -11,6 +11,8 @@ use std::{ sync::mpsc::{channel, RecvTimeoutError, Sender}, time::Duration, }; +#[cfg(windows)] +use tokio::runtime::Runtime; struct Handler { sp: EmptyExtraFieldService, @@ -18,6 +20,8 @@ struct Handler { tx_cb_result: Sender, #[cfg(target_os = "windows")] stream: Option>, + #[cfg(target_os = "windows")] + rt: Option, } pub fn new() -> GenericService { @@ -34,6 +38,8 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { tx_cb_result, #[cfg(target_os = "windows")] stream: None, + #[cfg(target_os = "windows")] + rt: None, }; let (tx_start_res, rx_start_res) = channel(); @@ -95,58 +101,85 @@ impl Handler { log::error!("Failed to read clipboard from cm: {}", e); } Ok(data) => { - let mut msg = Message::new(); - let multi_clipboards = MultiClipboards { - clipboards: data - .into_iter() - .map(|c| Clipboard { - compress: c.compress, - content: c.content, - width: c.width, - height: c.height, - format: ClipboardFormat::from_i32(c.format) - .unwrap_or(ClipboardFormat::Text) - .into(), - ..Default::default() - }) - .collect(), - ..Default::default() - }; - msg.set_multi_clipboards(multi_clipboards); - return Some(msg); + // Skip sending empty clipboard data. + // Maybe there's something wrong reading the clipboard data in cm, but no error msg is returned. + // The clipboard data should not be empty, the last line will try again to get the clipboard data. + if !data.is_empty() { + let mut msg = Message::new(); + let multi_clipboards = MultiClipboards { + clipboards: data + .into_iter() + .map(|c| Clipboard { + compress: c.compress, + content: c.content, + width: c.width, + height: c.height, + format: ClipboardFormat::from_i32(c.format) + .unwrap_or(ClipboardFormat::Text) + .into(), + special_name: c.special_name, + ..Default::default() + }) + .collect(), + ..Default::default() + }; + msg.set_multi_clipboards(multi_clipboards); + return Some(msg); + } } } } check_clipboard(&mut self.ctx, ClipboardSide::Host, false) } - // It's ok to do async operation in the clipboard service because: - // 1. the clipboard is not used frequently. - // 2. the clipboard handle is sync and will not block the main thread. + // Read clipboard data from cm using ipc. + // + // We cannot use `#[tokio::main(flavor = "current_thread")]` here, + // because the auto-managed tokio runtime (async context) will be dropped after the call. + // The next call will create a new runtime, which will cause the previous stream to be unusable. + // So we need to manage the tokio runtime manually. #[cfg(windows)] - #[tokio::main(flavor = "current_thread")] - async fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + if self.rt.is_none() { + self.rt = Some(Runtime::new()?); + } + let Some(rt) = &self.rt else { + // unreachable! + bail!("failed to get tokio runtime"); + }; let mut is_sent = false; if let Some(stream) = &mut self.stream { // If previous stream is still alive, reuse it. // If the previous stream is dead, `is_sent` will trigger reconnect. - is_sent = stream.send(&Data::ClipboardNonFile(None)).await.is_ok(); + is_sent = match rt.block_on(stream.send(&Data::ClipboardNonFile(None))) { + Ok(_) => true, + Err(e) => { + log::debug!("Failed to send to cm: {}", e); + false + } + }; } if !is_sent { - let mut stream = crate::ipc::connect(100, "_cm").await?; - stream.send(&Data::ClipboardNonFile(None)).await?; + let mut stream = rt.block_on(crate::ipc::connect(100, "_cm"))?; + rt.block_on(stream.send(&Data::ClipboardNonFile(None)))?; self.stream = Some(stream); } if let Some(stream) = &mut self.stream { loop { - match stream.next_timeout(800).await? { + match rt.block_on(stream.next_timeout(800))? { Some(Data::ClipboardNonFile(Some((err, mut contents)))) => { if !err.is_empty() { bail!("{}", err); } else { if contents.iter().any(|c| c.next_raw) { - match timeout(1000, stream.next_raw()).await { + // Wrap the future with a `Timeout` in an async block to avoid panic. + // We cannot use `rt.block_on(timeout(1000, stream.next_raw()))` here, because it causes panic: + // thread '' panicked at D:\Projects\rust\rustdesk\libs\hbb_common\src\lib.rs:98:5: + // there is no reactor running, must be called from the context of a Tokio 1.x runtime + // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + match rt.block_on(async { timeout(1000, stream.next_raw()).await }) + { Ok(Ok(mut data)) => { for c in &mut contents { if c.next_raw { @@ -163,7 +196,7 @@ impl Handler { Err(e) => { // Reconnect to avoid the next raw data remaining in the buffer. self.stream = None; - log::debug!("failed to get raw clipboard data: {}", e); + log::debug!("Failed to get raw clipboard data: {}", e); } } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 7b160ec2116..c0cf8c784e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -27,7 +27,7 @@ use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ - config::{self, Config, TrustedDevice}, + config::{self, keys, Config, TrustedDevice}, fs::{self, can_enable_overwrite_detection}, futures::{SinkExt, StreamExt}, get_time, get_version_number, @@ -64,9 +64,9 @@ pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); - static ref SESSIONS: Arc::>> = Default::default(); + static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref AUTHED_CONNS: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } @@ -140,10 +140,15 @@ enum MessageInput { BlockOffPlugin(String), } -#[derive(Clone, Debug)] -struct Session { +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct SessionKey { + peer_id: String, name: String, session_id: u64, +} + +#[derive(Clone, Debug)] +struct Session { last_recv_time: Arc>, random_password: String, tfa: bool, @@ -210,7 +215,7 @@ pub struct Connection { server_audit_conn: String, server_audit_file: String, lr: LoginRequest, - last_recv_time: Arc>, + session_last_recv_time: Option>>, chat_unanswered: bool, file_transferred: bool, #[cfg(windows)] @@ -335,7 +340,7 @@ impl Connection { clipboard: Connection::permission("enable-clipboard"), audio: Connection::permission("enable-audio"), // to-do: make sure is the option correct here - file: Connection::permission(config::keys::OPTION_ENABLE_FILE_TRANSFER), + file: Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER), restart: Connection::permission("enable-remote-restart"), recording: Connection::permission("enable-record-session"), block_input: Connection::permission("enable-block-input"), @@ -357,7 +362,7 @@ impl Connection { server_audit_conn: "".to_owned(), server_audit_file: "".to_owned(), lr: Default::default(), - last_recv_time: Arc::new(Mutex::new(Instant::now())), + session_last_recv_time: None, chat_unanswered: false, file_transferred: false, #[cfg(windows)] @@ -588,7 +593,7 @@ impl Connection { }, Ok(bytes) => { last_recv_time = Instant::now(); - *conn.last_recv_time.lock().unwrap() = Instant::now(); + conn.session_last_recv_time.as_mut().map(|t| *t.lock().unwrap() = Instant::now()); if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { if !conn.on_message(msg_in).await { break; @@ -755,6 +760,7 @@ impl Connection { } if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { conn.on_close(&err.to_string(), false).await; + raii::AuthedConnID::check_remove_session(conn.inner.id(), conn.session_key()); } conn.post_conn_audit(json!({ @@ -1131,7 +1137,13 @@ impl Connection { self.authed_conn_id = Some(self::raii::AuthedConnID::new( self.inner.id(), auth_conn_type, + self.session_key(), )); + self.session_last_recv_time = SESSIONS + .lock() + .unwrap() + .get(&self.session_key()) + .map(|s| s.last_recv_time.clone()); self.post_conn_audit( json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), ); @@ -1279,29 +1291,9 @@ impl Connection { self.send(msg_out).await; } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - #[cfg(not(windows))] - let displays = display_service::try_get_displays(); - #[cfg(windows)] - let displays = display_service::try_get_displays_add_amyuni_headless(); - pi.resolutions = Some(SupportedResolutions { - resolutions: displays - .map(|displays| { - displays - .get(self.display_idx) - .map(|d| crate::platform::resolutions(&d.name())) - .unwrap_or(vec![]) - }) - .unwrap_or(vec![]), - ..Default::default() - }) - .into(); - } - try_activate_screen(); - match super::display_service::update_get_sync_displays().await { + match super::display_service::update_get_sync_displays_on_login().await { Err(err) => { res.set_error(format!("{}", err)); } @@ -1314,6 +1306,18 @@ impl Connection { } pi.displays = displays; pi.current_display = self.display_idx as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: pi + .displays + .get(self.display_idx) + .map(|d| crate::platform::resolutions(&d.name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } res.set_peer_info(pi); sub_service = true; @@ -1367,7 +1371,10 @@ impl Connection { if !self.follow_remote_window { noperms.push(NAME_WINDOW_FOCUS); } - if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { + if !self.clipboard_enabled() + || !self.peer_keyboard_enabled() + || crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) == "Y" + { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { @@ -1546,15 +1553,10 @@ impl Connection { if password::temporary_enabled() { let password = password::temporary_password(); if self.validate_one_password(password.clone()) { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), - Session { - name: self.lr.my_name.clone(), - session_id: self.lr.session_id, - last_recv_time: self.last_recv_time.clone(), - random_password: password, - tfa: false, - }, + raii::AuthedConnID::update_or_insert_session( + self.session_key(), + Some(password), + Some(false), ); return true; } @@ -1575,21 +1577,15 @@ impl Connection { let session = SESSIONS .lock() .unwrap() - .get(&self.lr.my_id) + .get(&self.session_key()) .map(|s| s.to_owned()); // last_recv_time is a mutex variable shared with connection, can be updated lively. - if let Some(mut session) = session { - if session.name == self.lr.my_name - && session.session_id == self.lr.session_id - && !self.lr.password.is_empty() + if let Some(session) = session { + if !self.lr.password.is_empty() && (tfa && session.tfa || !tfa && self.validate_one_password(session.random_password.clone())) { - session.last_recv_time = self.last_recv_time.clone(); - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); + log::info!("is recent session"); return true; } } @@ -1629,8 +1625,8 @@ impl Connection { #[inline] fn enable_trusted_devices() -> bool { config::option2bool( - config::keys::OPTION_ENABLE_TRUSTED_DEVICES, - &Config::get_option(config::keys::OPTION_ENABLE_TRUSTED_DEVICES), + keys::OPTION_ENABLE_TRUSTED_DEVICES, + &Config::get_option(keys::OPTION_ENABLE_TRUSTED_DEVICES), ) } @@ -1670,9 +1666,11 @@ impl Connection { .await { log::error!("ipc to connection manager exit: {}", err); + // https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525725, cm may start failed #[cfg(windows)] if !crate::platform::is_prelogin() && !err.to_string().contains(crate::platform::EXPLORER_EXE) + && !crate::hbbs_http::sync::is_pro() { allow_err!(tx_from_cm_clone.send(Data::CmErr(err.to_string()))); } @@ -1695,7 +1693,7 @@ impl Connection { } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { - if !Connection::permission(config::keys::OPTION_ENABLE_FILE_TRANSFER) { + if !Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER) { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; @@ -1768,7 +1766,9 @@ impl Connection { self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) .await; return false; - } else if password::approve_mode() == ApproveMode::Click + } else if (password::approve_mode() == ApproveMode::Click + && !(crate::platform::is_prelogin() + && crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y")) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); @@ -1836,35 +1836,13 @@ impl Connection { if res { self.update_failure(failure, true, 1); self.require_2fa.take(); + raii::AuthedConnID::set_session_2fa(self.session_key()); self.send_logon_response().await; self.try_start_cm( self.lr.my_id.to_owned(), self.lr.my_name.to_owned(), self.authorized, ); - let session = SESSIONS - .lock() - .unwrap() - .get(&self.lr.my_id) - .map(|s| s.to_owned()); - if let Some(mut session) = session { - session.tfa = true; - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); - } else { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), - Session { - name: self.lr.my_name.clone(), - session_id: self.lr.session_id, - last_recv_time: self.last_recv_time.clone(), - random_password: "".to_owned(), - tfa: true, - }, - ); - } if !tfa.hwid.is_empty() && Self::enable_trusted_devices() { Config::add_trusted_device(TrustedDevice { hwid: tfa.hwid, @@ -2139,6 +2117,32 @@ impl Connection { } return true; } + if crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { + let mut job_id = None; + match &fa.union { + Some(file_action::Union::Send(s)) => { + job_id = Some(s.id); + } + Some(file_action::Union::RemoveFile(rf)) => { + job_id = Some(rf.id); + } + Some(file_action::Union::Rename(r)) => { + job_id = Some(r.id); + } + Some(file_action::Union::Create(c)) => { + job_id = Some(c.id); + } + Some(file_action::Union::RemoveDir(rd)) => { + job_id = Some(rd.id); + } + _ => {} + } + if let Some(job_id) = job_id { + self.send(fs::new_error(job_id, "one-way-file-transfer-tip", 0)) + .await; + return true; + } + } match fa.union { Some(file_action::Union::ReadDir(rd)) => { self.read_dir(&rd.path, rd.include_hidden); @@ -2370,7 +2374,10 @@ impl Connection { } Some(misc::Union::CloseReason(_)) => { self.on_close("Peer close", true).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::check_remove_session( + self.inner.id(), + self.session_key(), + ); return false; } @@ -2719,7 +2726,7 @@ impl Connection { } } } else { - if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false) { + if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false, true) { log::error!("Failed to plug out virtual display {}: {}", t.display, e); self.send(make_msg(format!( "Failed to plug out virtual displays: {}", @@ -3130,7 +3137,7 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); } fn read_dir(&mut self, dir: &str, include_hidden: bool) { @@ -3284,6 +3291,15 @@ impl Connection { } } } + + #[inline] + fn session_key(&self) -> SessionKey { + SessionKey { + peer_id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + session_id: self.lr.session_id, + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3781,15 +3797,18 @@ mod raii { pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { - pub fn new(id: i32, conn_type: AuthConnType) -> Self { - AUTHED_CONNS.lock().unwrap().push((id, conn_type)); + pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { + AUTHED_CONNS + .lock() + .unwrap() + .push((conn_id, conn_type, session_key)); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); - Self(id, conn_type) + Self(conn_id, conn_type) } fn check_wake_lock() { @@ -3814,6 +3833,72 @@ mod raii { .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) .count() } + + pub fn check_remove_session(conn_id: i32, key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let contains = lock.contains_key(&key); + if contains { + // If there are 2 connections with the same peer_id and session_id, a remote connection and a file transfer or port forward connection, + // If any of the connections is closed allowing retry, this will not be called; + // If the file transfer/port forward connection is closed with no retry, the session should be kept for remote control menu action; + // If the remote connection is closed with no retry, keep the session is not reasonable in case there is a retry button in the remote side, and ignore network fluctuations. + let another_remote = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .any(|c| c.0 != conn_id && c.2 == key && c.1 == AuthConnType::Remote); + if !another_remote { + lock.remove(&key); + log::info!("remove session"); + } else { + // Keep the session if there is another remote connection with same peer_id and session_id. + log::info!("skip remove session"); + } + } + } + + pub fn update_or_insert_session( + key: SessionKey, + password: Option, + tfa: Option, + ) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + if let Some(password) = password { + session.random_password = password; + } + if let Some(tfa) = tfa { + session.tfa = tfa; + } + } else { + lock.insert( + key, + Session { + random_password: password.unwrap_or_default(), + tfa: tfa.unwrap_or_default(), + last_recv_time: Arc::new(Mutex::new(Instant::now())), + }, + ); + } + } + + pub fn set_session_2fa(key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + session.tfa = true; + } else { + lock.insert( + key, + Session { + last_recv_time: Arc::new(Mutex::new(Instant::now())), + random_password: "".to_owned(), + tfa: true, + }, + ); + } + } } impl Drop for AuthedConnID { @@ -3821,7 +3906,7 @@ mod raii { if self.1 == AuthConnType::Remote { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); } - AUTHED_CONNS.lock().unwrap().retain(|&c| c.0 != self.0); + AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 7260b9c7a23..98b42a5face 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -1,4 +1,5 @@ use super::*; +use crate::common::SimpleCallOnReturn; #[cfg(target_os = "linux")] use crate::platform::linux::is_x11; #[cfg(windows)] @@ -7,6 +8,7 @@ use crate::virtual_display_manager; use hbb_common::get_version_number; use hbb_common::protobuf::MessageField; use scrap::Display; +use std::sync::atomic::{AtomicBool, Ordering}; // https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call @@ -29,6 +31,9 @@ lazy_static::lazy_static! { static ref SYNC_DISPLAYS: Arc> = Default::default(); } +// https://github.com/rustdesk/rustdesk/pull/8537 +static TEMP_IGNORE_DISPLAYS_CHANGED: AtomicBool = AtomicBool::new(false); + #[derive(Default)] struct SyncDisplaysInfo { displays: Vec, @@ -39,13 +44,17 @@ impl SyncDisplaysInfo { fn check_changed(&mut self, displays: Vec) { if self.displays.len() != displays.len() { self.displays = displays; - self.is_synced = false; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } return; } for (i, d) in displays.iter().enumerate() { if d != &self.displays[i] { self.displays = displays; - self.is_synced = false; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } return; } } @@ -60,6 +69,21 @@ impl SyncDisplaysInfo { } } +pub fn temp_ignore_displays_changed() -> SimpleCallOnReturn { + TEMP_IGNORE_DISPLAYS_CHANGED.store(true, std::sync::atomic::Ordering::Relaxed); + SimpleCallOnReturn { + b: true, + f: Box::new(move || { + // Wait for a while to make sure check_display_changed() is called + // after video service has sending its `SwitchDisplay` message(`try_broadcast_display_changed()`). + std::thread::sleep(Duration::from_millis(1000)); + TEMP_IGNORE_DISPLAYS_CHANGED.store(false, Ordering::Relaxed); + // Trigger the display changed message. + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + }), + } +} + // This function is really useful, though a duplicate check if display changed. // The video server will then send the following messages to the client: // 1. the supported resolutions of the {idx} display @@ -204,9 +228,11 @@ fn get_displays_msg() -> Option { fn run(sp: EmptyExtraFieldService) -> ResultType<()> { while sp.ok() { sp.snapshot(|sps| { - if sps.has_subscribes() { - SYNC_DISPLAYS.lock().unwrap().is_synced = false; - bail!("new subscriber"); + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + if sps.has_subscribes() { + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + bail!("new subscriber"); + } } Ok(()) })?; @@ -318,14 +344,18 @@ pub fn is_inited_msg() -> Option { None } -pub async fn update_get_sync_displays() -> ResultType> { +pub async fn update_get_sync_displays_on_login() -> ResultType> { #[cfg(target_os = "linux")] { if !is_x11() { return super::wayland::get_displays().await; } } - check_update_displays(&try_get_displays()?); + #[cfg(not(windows))] + let displays = display_service::try_get_displays(); + #[cfg(windows)] + let displays = display_service::try_get_displays_add_amyuni_headless(); + check_update_displays(&displays?); Ok(SYNC_DISPLAYS.lock().unwrap().displays.clone()) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2f9b86480a2..c7f651e9ac7 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -16,7 +16,7 @@ use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; #[cfg(target_os = "macos")] use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; #[cfg(target_os = "linux")] -use scrap::wayland::pipewire::RDP_RESPONSE; +use scrap::wayland::pipewire::RDP_SESSION_INFO; use std::{ convert::TryFrom, ops::{Deref, DerefMut, Sub}, @@ -175,6 +175,22 @@ impl LockModesHandler { } } + #[cfg(target_os = "linux")] + fn sleep_to_ensure_locked(v: bool, k: enigo::Key, en: &mut Enigo) { + if wayland_use_uinput() { + // Sleep at most 500ms to ensure the lock state is applied. + for _ in 0..50 { + std::thread::sleep(std::time::Duration::from_millis(10)); + if en.get_key_state(k) == v { + break; + } + } + } else if wayland_use_rdp_input() { + // We can't call `en.get_key_state(k)` because there's no api for this. + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + #[cfg(any(target_os = "windows", target_os = "linux"))] fn new(key_event: &KeyEvent, is_numpad_key: bool) -> Self { let mut en = ENIGO.lock().unwrap(); @@ -183,12 +199,15 @@ impl LockModesHandler { let caps_lock_changed = event_caps_enabled != local_caps_enabled; if caps_lock_changed { en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_caps_enabled, enigo::Key::CapsLock, &mut en); } let mut num_lock_changed = false; + let mut event_num_enabled = false; if is_numpad_key { let local_num_enabled = en.get_key_state(enigo::Key::NumLock); - let event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); + event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); num_lock_changed = event_num_enabled != local_num_enabled; } else if is_legacy_mode(key_event) { #[cfg(target_os = "windows")] @@ -199,6 +218,8 @@ impl LockModesHandler { } if num_lock_changed { en.key_click(enigo::Key::NumLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_num_enabled, enigo::Key::NumLock, &mut en); } Self { @@ -236,6 +257,14 @@ impl LockModesHandler { #[cfg(any(target_os = "windows", target_os = "linux"))] impl Drop for LockModesHandler { fn drop(&mut self) { + // Do not change led state if is Wayland uinput. + // Because there must be a delay to ensure the lock state is applied on Wayland uinput, + // which may affect the user experience. + #[cfg(target_os = "linux")] + if wayland_use_uinput() { + return; + } + let mut en = ENIGO.lock().unwrap(); if self.caps_lock_changed { en.key_click(enigo::Key::CapsLock); @@ -385,6 +414,9 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> { let displays = super::display_service::get_sync_displays(); + if displays.len() <= 1 { + return Ok(()); + } let disp_idx = crate::get_focused_display(displays); if let Some(disp_idx) = disp_idx.map(|id| id as i32) { if state.is_changed(disp_idx) { @@ -421,6 +453,26 @@ const MOUSE_ACTIVE_DISTANCE: i32 = 5; static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); +// https://github.com/rustdesk/rustdesk/issues/9729 +// We need to do some special handling for macOS when using the legacy mode. +#[cfg(target_os = "macos")] +static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(true); +// We use enigo to +// 1. Simulate mouse events +// 2. Simulate the legacy mode key events +// 3. Simulate the functioin key events, like LockScreen +#[inline] +#[cfg(target_os = "macos")] +fn enigo_ignore_flags() -> bool { + !LAST_KEY_LEGACY_MODE.load(Ordering::SeqCst) +} +#[inline] +#[cfg(target_os = "macos")] +fn set_last_legacy_mode(v: bool) { + LAST_KEY_LEGACY_MODE.store(v, Ordering::SeqCst); + ENIGO.lock().unwrap().set_ignore_flags(!v); +} + pub fn try_start_record_cursor_pos() -> Option> { if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { return None; @@ -473,6 +525,19 @@ impl VirtualInputState { fn new() -> Option { VirtualInput::new( CGEventSourceStateID::CombinedSessionState, + // Note: `CGEventTapLocation::Session` will be affected by the mouse events. + // When we're simulating key events, then move the physical mouse, the key events will be affected. + // It looks like https://github.com/rustdesk/rustdesk/issues/9729#issuecomment-2432306822 + // 1. Press "Command" key in RustDesk + // 2. Move the physical mouse + // 3. Press "V" key in RustDesk + // Then the controlled side just prints "v" instead of pasting. + // + // Changing `CGEventTapLocation::Session` to `CGEventTapLocation::HID` fixes it. + // But we do not consider this as a bug, because it's not a common case, + // we consider only RustDesk operates the controlled side. + // + // https://developer.apple.com/documentation/coregraphics/cgeventtaplocation/ CGEventTapLocation::Session, ) .map(|virtual_input| Self { @@ -518,15 +583,25 @@ pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultT #[cfg(target_os = "linux")] pub async fn setup_rdp_input() -> ResultType<(), Box> { let mut en = ENIGO.lock()?; - let rdp_res_lock = RDP_RESPONSE.lock()?; - let rdp_res = rdp_res_lock.as_ref().ok_or("RDP response is None")?; + let rdp_info_lock = RDP_SESSION_INFO.lock()?; + let rdp_info = rdp_info_lock.as_ref().ok_or("RDP session is None")?; - let keyboard = RdpInputKeyboard::new(rdp_res.conn.clone(), rdp_res.session.clone())?; + let keyboard = RdpInputKeyboard::new(rdp_info.conn.clone(), rdp_info.session.clone())?; en.set_custom_keyboard(Box::new(keyboard)); log::info!("RdpInput keyboard created"); - if let Some(stream) = rdp_res.streams.clone().into_iter().next() { - let mouse = RdpInputMouse::new(rdp_res.conn.clone(), rdp_res.session.clone(), stream)?; + if let Some(stream) = rdp_info.streams.clone().into_iter().next() { + let resolution = rdp_info + .resolution + .lock() + .unwrap() + .unwrap_or(stream.get_size()); + let mouse = RdpInputMouse::new( + rdp_info.conn.clone(), + rdp_info.session.clone(), + stream, + resolution, + )?; en.set_custom_mouse(Box::new(mouse)); log::info!("RdpInput mouse created"); } @@ -903,6 +978,8 @@ pub fn handle_mouse_(evt: &MouseEvent, conn: i32) { let buttons = evt.mask >> 3; let evt_type = evt.mask & 0x7; let mut en = ENIGO.lock().unwrap(); + #[cfg(target_os = "macos")] + en.set_ignore_flags(enigo_ignore_flags()); #[cfg(not(target_os = "macos"))] let mut to_release = Vec::new(); if evt_type == MOUSE_TYPE_DOWN { @@ -1445,17 +1522,27 @@ fn translate_keyboard_mode(evt: &KeyEvent) { en.key_sequence(seq); #[cfg(any(target_os = "linux", target_os = "windows"))] { - if get_modifier_state(Key::Shift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); - } - if get_modifier_state(Key::RightShift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + #[cfg(target_os = "windows")] + let simulate_win_hot_key = is_hot_key_modifiers_down(&mut en); + #[cfg(target_os = "linux")] + let simulate_win_hot_key = false; + if !simulate_win_hot_key { + if get_modifier_state(Key::Shift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + if get_modifier_state(Key::RightShift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } } for chr in seq.chars() { // char in rust is 4 bytes. // But for this case, char comes from keyboard. We only need 2 bytes. #[cfg(target_os = "windows")] - rdev::simulate_unicode(chr as _).ok(); + if simulate_win_hot_key { + rdev::simulate_char(chr, true).ok(); + } else { + rdev::simulate_unicode(chr as _).ok(); + } #[cfg(target_os = "linux")] en.key_click(Key::Layout(chr)); } @@ -1480,6 +1567,17 @@ fn translate_keyboard_mode(evt: &KeyEvent) { } } +#[inline] +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down(en: &mut Enigo) -> bool { + en.get_key_state(Key::Control) + || en.get_key_state(Key::RightControl) + || en.get_key_state(Key::Alt) + || en.get_key_state(Key::RightAlt) + || en.get_key_state(Key::Meta) + || en.get_key_state(Key::RWin) +} + #[cfg(target_os = "windows")] fn simulate_win2win_hotkey(code: u32, down: bool) { let unicode: u16 = (code & 0x0000FFFF) as u16; @@ -1604,16 +1702,24 @@ pub fn handle_key_(evt: &KeyEvent) { } } _ => {} - }; + } match evt.mode.enum_value() { Ok(KeyboardMode::Map) => { + #[cfg(target_os = "macos")] + set_last_legacy_mode(false); map_keyboard_mode(evt); } Ok(KeyboardMode::Translate) => { + #[cfg(target_os = "macos")] + set_last_legacy_mode(false); translate_keyboard_mode(evt); } _ => { + // All key down events are started from here, + // so we can reset the flag of last legacy mode here. + #[cfg(target_os = "macos")] + set_last_legacy_mode(true); legacy_keyboard_mode(evt); } } diff --git a/src/server/rdp_input.rs b/src/server/rdp_input.rs index 1a0a64054b9..910a192761d 100644 --- a/src/server/rdp_input.rs +++ b/src/server/rdp_input.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; use std::sync::Arc; pub mod client { + use hbb_common::platform::linux::is_kde; + use super::*; const EVDEV_MOUSE_LEFT: i32 = 272; @@ -67,6 +69,8 @@ pub mod client { conn: Arc, session: Path<'static>, stream: PwStreamInfo, + resolution: (usize, usize), + scale: Option, } impl RdpInputMouse { @@ -74,11 +78,32 @@ pub mod client { conn: Arc, session: Path<'static>, stream: PwStreamInfo, + resolution: (usize, usize), ) -> ResultType { + // https://github.com/rustdesk/rustdesk/pull/9019#issuecomment-2295252388 + // There may be a bug in Rdp input on Gnome util Ubuntu 24.04 (Gnome 46) + // + // eg. Resultion 800x600, Fractional scale: 200% (logic size: 400x300) + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.impl.portal.RemoteDesktop.html#:~:text=new%20pointer%20position-,in%20the%20streams%20logical%20coordinate%20space,-. + // Then (x,y) in `mouse_move_to()` and `mouse_move_relative()` should be scaled to the logic size(stream.get_size()), which is from (0,0) to (400,300). + // For Ubuntu 24.04(Gnome 46), (x,y) is restricted from (0,0) to (400,300), but the actual range in screen is: + // Logic coordinate from (0,0) to (200x150). + // Or physical coordinate from (0,0) to (400,300). + let scale = if is_kde() { + if resolution.0 == 0 || stream.get_size().0 == 0 { + Some(1.0f64) + } else { + Some(resolution.0 as f64 / stream.get_size().0 as f64) + } + } else { + None + }; Ok(Self { conn, session, stream, + resolution, + scale, }) } } @@ -93,24 +118,44 @@ pub mod client { } fn mouse_move_to(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; let portal = get_portal(&self.conn); let _ = remote_desktop_portal::notify_pointer_motion_absolute( &portal, &self.session, HashMap::new(), self.stream.path as u32, - x as f64, - y as f64, + x, + y, ); } fn mouse_move_relative(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; let portal = get_portal(&self.conn); let _ = remote_desktop_portal::notify_pointer_motion( &portal, &self.session, HashMap::new(), - x as f64, - y as f64, + x, + y, ); } fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType { diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 942f3753a76..60c647862ad 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -431,8 +431,8 @@ pub mod service { allow_err!(keyboard.emit(&[down_event])); } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { - let down_event = InputEvent::new(EventType::KEY, *code - 8, 0); - allow_err!(keyboard.emit(&[down_event])); + let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); + allow_err!(keyboard.emit(&[up_event])); } DataKeyboard::KeyDown(key) => { if let Ok((k, is_shift)) = map_key(key) { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 8b326a2ffdd..55bfa08f0e6 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -486,6 +486,9 @@ fn run(vs: VideoService) -> ResultType<()> { let mut repeat_encode_counter = 0; let repeat_encode_max = 10; let mut encode_fail_counter = 0; + let mut first_frame = true; + let capture_width = c.width; + let capture_height = c.height; while sp.ok() { #[cfg(windows)] @@ -574,6 +577,9 @@ fn run(vs: VideoService) -> ResultType<()> { &mut encoder, recorder.clone(), &mut encode_fail_counter, + &mut first_frame, + capture_width, + capture_height, )?; frame_controller.set_send(now, send_conn_ids); } @@ -629,6 +635,9 @@ fn run(vs: VideoService) -> ResultType<()> { &mut encoder, recorder.clone(), &mut encode_fail_counter, + &mut first_frame, + capture_width, + capture_height, )?; frame_controller.set_send(now, send_conn_ids); } @@ -719,7 +728,13 @@ fn setup_encoder( ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder(c.width, c.height, &codec_format, record_incoming); + let recorder = get_recorder( + c.width, + c.height, + &codec_format, + record_incoming, + display_idx, + ); let use_i444 = Encoder::use_i444(&encoder_cfg); let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?; Ok((encoder, encoder_cfg, codec_format, use_i444, recorder)) @@ -806,6 +821,7 @@ fn get_recorder( height: usize, codec_format: &CodecFormat, record_incoming: bool, + display: usize, ) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); @@ -825,10 +841,7 @@ fn get_recorder( server: true, id: Config::get_id(), dir: crate::ui_interface::video_save_directory(root), - filename: "".to_owned(), - width, - height, - format: codec_format.clone(), + display, tx, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) @@ -906,6 +919,9 @@ fn handle_one_frame( encoder: &mut Encoder, recorder: Arc>>, encode_fail_counter: &mut usize, + first_frame: &mut bool, + width: usize, + height: usize, ) -> ResultType> { sp.snapshot(|sps| { // so that new sub and old sub share the same encoder after switch @@ -917,6 +933,8 @@ fn handle_one_frame( })?; let mut send_conn_ids: HashSet = Default::default(); + let first = *first_frame; + *first_frame = false; match encoder.encode_to_message(frame, ms) { Ok(mut vf) => { *encode_fail_counter = 0; @@ -927,21 +945,27 @@ fn handle_one_frame( .lock() .unwrap() .as_mut() - .map(|r| r.write_message(&msg)); + .map(|r| r.write_message(&msg, width, height)); send_conn_ids = sp.send_video_frame(msg); } Err(e) => { + *encode_fail_counter += 1; + // Encoding errors are not frequent except on Android + if !cfg!(target_os = "android") { + log::error!("encode fail: {e:?}, times: {}", *encode_fail_counter,); + } let max_fail_times = if cfg!(target_os = "android") && encoder.is_hardware() { - 12 + 9 } else { - 6 + 3 }; - *encode_fail_counter += 1; - if *encode_fail_counter >= max_fail_times { + let repeat = !encoder.latency_free(); + // repeat encoders can reach max_fail_times on the first frame + if (first && !repeat) || *encode_fail_counter >= max_fail_times { *encode_fail_counter = 0; if encoder.is_hardware() { encoder.disable(); - log::error!("switch due to encoding fails more than {max_fail_times} times"); + log::error!("switch due to encoding fails, first frame: {first}, error: {e:?}"); bail!("SWITCH"); } } diff --git a/src/tray.rs b/src/tray.rs index 74c18bf7bd0..3a3ae92f37f 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -98,12 +98,11 @@ fn make_tray() -> hbb_common::ResultType<()> { crate::run_me::<&str>(vec![]).ok(); } #[cfg(target_os = "linux")] - if !std::process::Command::new("xdg-open") - .arg(&crate::get_uri_prefix()) - .spawn() - .is_ok() { - crate::run_me::<&str>(vec![]).ok(); + // Do not use "xdg-open", it won't read config + if crate::dbus::invoke_new_connection(crate::get_uri_prefix()).is_err() { + crate::run_me::<&str>(vec![]).ok(); + } } }; diff --git a/src/ui/header.tis b/src/ui/header.tis index c4e76528097..3116f1f542f 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -152,10 +152,13 @@ class Header: Reactor.Component { } function renderKeyboardPop(){ + const is_map_mode_supported = handler.is_keyboard_mode_supported("map"); + const is_translate_mode_supported = handler.is_keyboard_mode_supported("translate"); return -
  • {svg_checkmark}{translate('Legacy mode')}
  • -
  • {svg_checkmark}{translate('Map mode')}
  • +
  • {svg_checkmark}{translate('Legacy mode')}
  • + { is_map_mode_supported &&
  • {svg_checkmark}{translate('Map mode')}
  • } + { is_translate_mode_supported &&
  • {svg_checkmark}{translate('Translate mode')}
  • }
    ; } @@ -298,26 +301,12 @@ class Header: Reactor.Component { } event click $(span#recording) (_, me) { - recording = !recording; header.update(); - handler.record_status(recording); - // 0 is just a dummy value. It will be ignored by the handler. - if (recording) { - handler.refresh_video(0); - if (handler.version_cmp(pi.version, '1.2.4') >= 0) handler.record_screen(recording, pi.current_display, display_width, display_height); - } - else { - handler.record_screen(recording, pi.current_display, display_width, display_height); - } + handler.record_screen(!recording) } event click $(#screen) (_, me) { if (pi.current_display == me.index) return; - if (recording) { - recording = false; - handler.record_screen(false, pi.current_display, display_width, display_height); - handler.record_status(false); - } handler.switch_display(me.index); } @@ -443,7 +432,7 @@ function handle_custom_image_quality() { var extendedBitrate = bitrate > 100; var maxRate = extendedBitrate ? 2000 : 100; msgbox("custom-image-quality", "Custom Image Quality", "
    \ -
    x% Bitrate More
    \ +
    x% Bitrate More
    \
    ", "", function(res=null) { if (!res) return; if (res.id === "extended-slider") { @@ -515,6 +504,7 @@ if (!(is_file_transfer || is_port_forward)) { handler.updatePi = function(v) { pi = v; + recording = handler.is_recording(); header.update(); if (is_port_forward) { view.windowState = View.WINDOW_MINIMIZED; @@ -679,3 +669,8 @@ handler.setConnectionType = function(secured, direct) { direct_connection: direct, }); } + +handler.updateRecordStatus = function(status) { + recording = status; + header.update(); +} \ No newline at end of file diff --git a/src/ui/index.tis b/src/ui/index.tis index b10df76233e..3ae54637f4b 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -253,10 +253,12 @@ class Enhancements: Reactor.Component { var root_dir = show_root_dir ? handler.video_save_directory(true) : ""; var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + var ts2 = handler.get_local_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; msgbox("custom-recording", translate('Recording'),
    {translate('Enable recording session')}
    {translate('Automatically record incoming sessions')}
    +
    {translate('Automatically record outgoing sessions')}
    {show_root_dir ?
    {translate("Incoming")}:  {root_dir}
    : ""}
    {translate(show_root_dir ? "Outgoing" : "Directory")}:  {user_dir}
    @@ -267,7 +269,8 @@ class Enhancements: Reactor.Component { if (!res) return; handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); - handler.set_option("video-save-directory", $(#folderPath).text); + handler.set_local_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : ''); + handler.set_local_option("video-save-directory", $(#folderPath).text); }); } this.toggleMenuState(); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0ad84e8ed15..0296d82bda5 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -335,6 +335,10 @@ impl InvokeUiSession for SciterHandler { } fn next_rgba(&self, _display: usize) {} + + fn update_record_status(&self, start: bool) { + self.call("updateRecordStatus", &make_args!(start)); + } } pub struct SciterSession(Session); @@ -478,8 +482,7 @@ impl sciter::EventHandler for SciterSession { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(i32); - fn record_screen(bool, i32, i32, i32); - fn record_status(bool); + fn record_screen(bool); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); @@ -487,6 +490,7 @@ impl sciter::EventHandler for SciterSession { fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); fn get_keyboard_mode(); + fn is_keyboard_mode_supported(String); fn save_keyboard_mode(String); fn alternative_codecs(); fn change_prefer_codec(); @@ -495,13 +499,14 @@ impl sciter::EventHandler for SciterSession { fn close_voice_call(); fn version_cmp(String, String); fn set_selected_windows_session_id(String); + fn is_recording(); } } impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let force_relay = args.contains(&"--relay".to_string()); - let mut session: Session = Session { + let session: Session = Session { password: password.clone(), args, server_keyboard_enabled: Arc::new(RwLock::new(true)), @@ -524,7 +529,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, force_relay, None, None); + .initialize(id, conn_type, None, force_relay, None, None, None); Self(session) } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 89e9ceabbea..c34e15e26c8 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -440,7 +440,7 @@ impl IpcTaskRunner { Data::ClipboardFile(_clip) => { #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] { - let is_stopping_allowed = _clip.is_stopping_allowed_from_peer(); + let is_stopping_allowed = _clip.is_beginning_message(); let is_clipboard_enabled = ContextSend::is_enabled(); let file_transfer_enabled = self.file_transfer_enabled; let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); @@ -498,10 +498,10 @@ impl IpcTaskRunner { let (content, next_raw) = { // TODO: find out a better threshold if content_len > 1024 * 3 { - (c.content, false) - } else { raw_contents.extend(c.content); (bytes::Bytes::new(), true) + } else { + (c.content, false) } }; main_data.push(ClipboardNonFile { @@ -512,12 +512,16 @@ impl IpcTaskRunner { width: c.width, height: c.height, format: c.format.value(), + special_name: c.special_name, }); } allow_err!(self.stream.send(&Data::ClipboardNonFile(Some(("".to_owned(), main_data)))).await); - allow_err!(self.stream.send_raw(raw_contents.into()).await); + if !raw_contents.is_empty() { + allow_err!(self.stream.send_raw(raw_contents.into()).await); + } } Err(e) => { + log::debug!("Failed to get clipboard content. {}", e); allow_err!(self.stream.send(&Data::ClipboardNonFile(Some((format!("{}", e), vec![])))).await); } } @@ -565,7 +569,12 @@ impl IpcTaskRunner { if stop { ContextSend::set_is_stopped(); } else { - allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + if _clip.is_beginning_message() && crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { + // If one way file transfer is enabled, don't send clipboard file to client + // Don't call `ContextSend::set_is_stopped()`, because it will stop bidirectional file copy&paste. + } else { + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + } } } } @@ -623,7 +632,6 @@ pub async fn start_ipc(cm: ConnectionManager) { OPTION_ENABLE_FILE_TRANSFER, &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), )); - match ipc::new_listener("_cm").await { Ok(mut incoming) => { while let Some(result) = incoming.next().await { @@ -645,7 +653,7 @@ pub async fn start_ipc(cm: ConnectionManager) { log::error!("Failed to start cm ipc server: {}", err); } } - crate::platform::quit_gui(); + quit_cm(); } #[cfg(target_os = "android")] @@ -1040,3 +1048,11 @@ pub fn close_voice_call(id: i32) { allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; } + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn quit_cm() { + // in case of std::process::exit not work + log::info!("quit cm"); + CLIENTS.write().unwrap().clear(); + crate::platform::quit_gui(); +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9c3864f0c3c..bab54c79a3c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -207,17 +207,12 @@ pub fn get_hard_option(key: String) -> String { #[inline] pub fn get_builtin_option(key: &str) -> String { - config::BUILTIN_SETTINGS - .read() - .unwrap() - .get(key) - .cloned() - .unwrap_or_default() + crate::get_builtin_option(key) } #[inline] pub fn set_local_option(key: String, value: String) { - LocalConfig::set_option(key, value); + LocalConfig::set_option(key.clone(), value.clone()); } #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] @@ -692,7 +687,6 @@ pub fn create_shortcut(_id: String) { #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn discover() { - #[cfg(not(any(target_os = "ios")))] std::thread::spawn(move || { allow_err!(crate::lan::discover()); }); @@ -850,7 +844,11 @@ pub fn video_save_directory(root: bool) -> String { return dir.to_string_lossy().to_string(); } } - let dir = Config::get_option("video-save-directory"); + // Get directory from config file otherwise --server will use the old value from global var. + #[cfg(any(target_os = "linux", target_os = "macos"))] + let dir = LocalConfig::get_option_from_file(OPTION_VIDEO_SAVE_DIRECTORY); + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let dir = LocalConfig::get_option(OPTION_VIDEO_SAVE_DIRECTORY); if !dir.is_empty() { return dir; } @@ -1139,6 +1137,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { log::error!("ipc connection closed: {}", err); + if is_cm { + crate::ui_cm_interface::quit_cm(); + } break; } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1496,3 +1498,8 @@ pub fn clear_trusted_devices() { #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::clear_trusted_devices(); } + +#[cfg(feature = "flutter")] +pub fn max_encrypt_len() -> usize { + hbb_common::config::ENCRYPT_MAX_LEN +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 0e030aa8ba4..321707d3f63 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -252,6 +252,18 @@ impl Session { self.fallback_keyboard_mode() } + pub fn is_keyboard_mode_supported(&self, mode: String) -> bool { + if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { + crate::common::is_keyboard_mode_supported( + &mode, + self.get_peer_version(), + &self.peer_platform(), + ) + } else { + false + } + } + pub fn save_keyboard_mode(&self, value: String) { self.lc.write().unwrap().save_keyboard_mode(value); } @@ -377,22 +389,17 @@ impl Session { self.send(Data::Message(LoginConfigHandler::refresh())); } - pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) { - self.send(Data::RecordScreen( - start, - display as usize, - w, - h, - self.get_id(), - )); - } - - pub fn record_status(&self, status: bool) { + pub fn record_screen(&self, start: bool) { let mut misc = Misc::new(); - misc.set_client_record_status(status); + misc.set_client_record_status(start); let mut msg = Message::new(); msg.set_misc(misc); self.send(Data::Message(msg)); + self.send(Data::RecordScreen(start)); + } + + pub fn is_recording(&self) -> bool { + self.lc.read().unwrap().record } pub fn save_custom_image_quality(&self, custom_image_quality: i32) { @@ -788,7 +795,7 @@ impl Session { } #[cfg(any(target_os = "ios"))] - pub fn handle_flutter_key_event( + pub fn handle_flutter_raw_key_event( &self, _keyboard_mode: &str, _name: &str, @@ -800,7 +807,7 @@ impl Session { } #[cfg(not(any(target_os = "ios")))] - pub fn handle_flutter_key_event( + pub fn handle_flutter_raw_key_event( &self, keyboard_mode: &str, name: &str, @@ -812,7 +819,7 @@ impl Session { if name == "flutter_key" { self._handle_key_flutter_simulation(keyboard_mode, platform_code, down_or_up); } else { - self._handle_key_non_flutter_simulation( + self._handle_raw_key_non_flutter_simulation( keyboard_mode, platform_code, position_code, @@ -823,6 +830,65 @@ impl Session { } #[cfg(not(any(target_os = "ios")))] + fn _handle_raw_key_non_flutter_simulation( + &self, + keyboard_mode: &str, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if position_code < 0 || platform_code < 0 { + return; + } + let platform_code: u32 = platform_code as _; + let position_code: KeyCode = position_code as _; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_code(position_code) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(platform_code, position_code); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: SystemTime::now(), + unicode: None, + platform_code, + position_code: position_code as _, + event_type, + usb_hid: 0, + #[cfg(any(target_os = "windows", target_os = "macos"))] + extra_data: 0, + }; + keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self); + } + + pub fn handle_flutter_key_event( + &self, + keyboard_mode: &str, + character: &str, + usb_hid: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if character == "flutter_key" { + self._handle_key_flutter_simulation(keyboard_mode, usb_hid, down_or_up); + } else { + self._handle_key_non_flutter_simulation( + keyboard_mode, + character, + usb_hid, + lock_modes, + down_or_up, + ); + } + } + fn _handle_key_flutter_simulation( &self, _keyboard_mode: &str, @@ -831,10 +897,10 @@ impl Session { ) { // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/services/keyboard_key.g.dart#L4356 let ctrl_key = match platform_code { - 0x0007007f => Some(ControlKey::VolumeMute), - 0x00070080 => Some(ControlKey::VolumeUp), - 0x00070081 => Some(ControlKey::VolumeDown), - 0x00070066 => Some(ControlKey::Power), + 0x007f => Some(ControlKey::VolumeMute), + 0x0080 => Some(ControlKey::VolumeUp), + 0x0081 => Some(ControlKey::VolumeDown), + 0x0066 => Some(ControlKey::Power), _ => None, }; let Some(ctrl_key) = ctrl_key else { return }; @@ -847,26 +913,41 @@ impl Session { self.send_key_event(&key_event); } - #[cfg(not(any(target_os = "ios")))] fn _handle_key_non_flutter_simulation( &self, keyboard_mode: &str, - platform_code: i32, - position_code: i32, + character: &str, + usb_hid: i32, lock_modes: i32, down_or_up: bool, ) { - if position_code < 0 || platform_code < 0 { - return; - } - let platform_code: u32 = platform_code as _; - let position_code: KeyCode = position_code as _; + let key = rdev::usb_hid_key_from_code(usb_hid as _); + + #[cfg(any(target_os = "android", target_os = "ios"))] + let position_code: KeyCode = 0; + #[cfg(any(target_os = "android", target_os = "ios"))] + let platform_code: KeyCode = 0; - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_code(position_code) as rdev::Key; - // Windows requires special handling #[cfg(target_os = "windows")] - let key = rdev::get_win_key(platform_code, position_code); + let platform_code: u32 = rdev::win_code_from_key(key).unwrap_or(0); + #[cfg(target_os = "windows")] + let position_code: KeyCode = rdev::win_scancode_from_key(key).unwrap_or(0) as _; + + #[cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))] + let position_code: KeyCode = rdev::code_from_key(key).unwrap_or(0) as _; + #[cfg(not(any( + target_os = "windows", + target_os = "android", + target_os = "ios", + target_os = "linux" + )))] + let platform_code: u32 = position_code as _; + // For translate mode. + // We need to set the platform code (keysym) if is AltGr. + // https://github.com/rustdesk/rustdesk/blob/07cf1b4db5ef2f925efd3b16b87c33ce03c94809/src/keyboard.rs#L1029 + // https://github.com/flutter/flutter/issues/153811 + #[cfg(target_os = "linux")] + let platform_code: u32 = position_code as _; let event_type = if down_or_up { KeyPress(key) @@ -875,14 +956,27 @@ impl Session { }; let event = Event { time: SystemTime::now(), - unicode: None, + unicode: if character.is_empty() { + None + } else { + Some(rdev::UnicodeInfo { + name: Some(character.to_string()), + unicode: character.encode_utf16().collect(), + // is_dead: is not correct here, because flutter cannot detect deadcode for now. + is_dead: false, + }) + }, platform_code, position_code: position_code as _, event_type, + #[cfg(any(target_os = "android", target_os = "ios"))] + usb_hid: usb_hid as _, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + usb_hid: 0, #[cfg(any(target_os = "windows", target_os = "macos"))] extra_data: 0, }; - keyboard::client::process_event(keyboard_mode, &event, Some(lock_modes)); + keyboard::client::process_event_with_session(keyboard_mode, &event, Some(lock_modes), self); } // flutter only TODO new input @@ -1396,6 +1490,10 @@ impl Session { msg.set_misc(misc); self.send(Data::Message(msg)); } + + pub fn get_conn_token(&self) -> Option { + self.lc.read().unwrap().get_conn_token() + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1459,6 +1557,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_current_display(&self, disp_idx: i32); #[cfg(feature = "flutter")] fn is_multi_ui_session(&self) -> bool; + fn update_record_status(&self, start: bool); } impl Deref for Session { diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index 138087c75e8..41e5b3fc83c 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -8,6 +8,7 @@ pub const AMYUNI_IDD_DEVICE_STRING: &'static str = "USB Mobile Monitor Virtual D const IDD_IMPL: &str = IDD_IMPL_AMYUNI; const IDD_IMPL_RUSTDESK: &str = "rustdesk_idd"; const IDD_IMPL_AMYUNI: &str = "amyuni_idd"; +const IDD_PLUG_OUT_ALL_INDEX: i32 = -1; pub fn is_amyuni_idd() -> bool { IDD_IMPL == IDD_IMPL_AMYUNI @@ -76,17 +77,17 @@ pub fn plug_in_monitor(idx: u32, modes: Vec) -> Re } } -pub fn plug_out_monitor(index: i32, force_all: bool) -> ResultType<()> { +pub fn plug_out_monitor(index: i32, force_all: bool, force_one: bool) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => { - let indices = if index == -1 { + let indices = if index == IDD_PLUG_OUT_ALL_INDEX { rustdesk_idd::get_virtual_displays() } else { vec![index as _] }; rustdesk_idd::plug_out_peer_request(&indices) } - IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index, force_all), + IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index, force_all, force_one), _ => bail!("Unsupported virtual display implementation."), } } @@ -102,12 +103,16 @@ pub fn plug_in_peer_request(modes: Vec>) -> Re } } -pub fn plug_out_monitor_indices(indices: &[u32], force_all: bool) -> ResultType<()> { +pub fn plug_out_monitor_indices( + indices: &[u32], + force_all: bool, + force_one: bool, +) -> ResultType<()> { match IDD_IMPL { IDD_IMPL_RUSTDESK => rustdesk_idd::plug_out_peer_request(indices), IDD_IMPL_AMYUNI => { for _idx in indices.iter() { - amyuni_idd::plug_out_monitor(0, force_all)?; + amyuni_idd::plug_out_monitor(0, force_all, force_one)?; } Ok(()) } @@ -382,7 +387,7 @@ pub mod amyuni_idd { use hbb_common::{bail, lazy_static, log, tokio::time::Instant, ResultType}; use std::{ ptr::null_mut, - sync::{Arc, Mutex}, + sync::{atomic, Arc, Mutex}, time::Duration, }; use winapi::{ @@ -405,6 +410,14 @@ pub mod amyuni_idd { static ref LOCK: Arc> = Default::default(); static ref LAST_PLUG_IN_HEADLESS_TIME: Arc>> = Arc::new(Mutex::new(None)); } + const VIRTUAL_DISPLAY_MAX_COUNT: usize = 4; + // The count of virtual displays plugged in. + // This count is not accurate, because: + // 1. The virtual display driver may also be controlled by other processes. + // 2. RustDesk may crash and restart, but the virtual displays are kept. + // + // to-do: Maybe a better way is to add an option asking the user if plug out all virtual displays on disconnect. + static VIRTUAL_DISPLAY_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0); fn get_deviceinstaller64_work_dir() -> ResultType>> { let cur_exe = std::env::current_exe()?; @@ -510,7 +523,7 @@ pub mod amyuni_idd { pub fn reset_all() -> ResultType<()> { let _ = crate::privacy_mode::turn_off_privacy(0, None); - let _ = plug_out_monitor(-1, true); + let _ = plug_out_monitor(super::IDD_PLUG_OUT_ALL_INDEX, true, false); *LAST_PLUG_IN_HEADLESS_TIME.lock().unwrap() = None; Ok(()) } @@ -522,6 +535,18 @@ pub mod amyuni_idd { unsafe { win_device::device_io_control(&INTERFACE_GUID, PLUG_MONITOR_IO_CONTROL_CDOE, &cmd, 0)?; } + // No need to consider concurrency here. + if add { + // If the monitor is plugged in, increase the count. + // Though there's already a check of `VIRTUAL_DISPLAY_MAX_COUNT`, it's still better to check here for double ensure. + if VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::SeqCst) < VIRTUAL_DISPLAY_MAX_COUNT { + VIRTUAL_DISPLAY_COUNT.fetch_add(1, atomic::Ordering::SeqCst); + } + } else { + if VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::SeqCst) > 0 { + VIRTUAL_DISPLAY_COUNT.fetch_sub(1, atomic::Ordering::SeqCst); + } + } Ok(()) } @@ -607,44 +632,73 @@ pub mod amyuni_idd { bail!("Failed to install driver."); } - if get_monitor_count() == 4 { - bail!("There are already 4 monitors plugged in."); + if get_monitor_count() == VIRTUAL_DISPLAY_MAX_COUNT { + bail!("There are already {VIRTUAL_DISPLAY_MAX_COUNT} monitors plugged in."); } plug_in_monitor_(true, is_async) } - pub fn plug_out_monitor(index: i32, force_all: bool) -> ResultType<()> { - let all_count = windows::get_device_names(None).len(); + // `index` the display index to plug out. -1 means plug out all. + // `force_all` is used to forcibly plug out all virtual displays. + // `force_one` is used to forcibly plug out one virtual display managed by other processes + // if there're no virtual displays managed by RustDesk. + pub fn plug_out_monitor(index: i32, force_all: bool, force_one: bool) -> ResultType<()> { + let plug_out_all = index == super::IDD_PLUG_OUT_ALL_INDEX; + // If `plug_out_all and force_all` is true, forcibly plug out all virtual displays. + // Though the driver may be controlled by other processes, + // we still forcibly plug out all virtual displays. + // + // 1. RustDesk plug in 2 virtual displays. (RustDesk) + // 2. Other process plug out all virtual displays. (User mannually) + // 3. Other process plug in 1 virtual display. (User mannually) + // 4. RustDesk plug out all virtual displays in this call. (RustDesk disconnect) + // + // This is not a normal scenario, RustDesk will plug out virtual display unexpectedly. + let mut plug_in_count = VIRTUAL_DISPLAY_COUNT.load(atomic::Ordering::Relaxed); let amyuni_count = get_monitor_count(); + if !plug_out_all { + if plug_in_count == 0 && amyuni_count > 0 { + if force_one { + plug_in_count = 1; + } else { + bail!("The virtual display is managed by other processes."); + } + } + } else { + // Ignore the message if trying to plug out all virtual displays. + } + + let all_count = windows::get_device_names(None).len(); let mut to_plug_out_count = match all_count { 0 => return Ok(()), 1 => { - if amyuni_count == 0 { + if plug_in_count == 0 { bail!("No virtual displays to plug out.") } else { if force_all { 1 } else { - bail!("This only virtual display cannot be pulled out.") + bail!("This only virtual display cannot be plugged out.") } } } _ => { - if all_count == amyuni_count { + if all_count == plug_in_count { if force_all { all_count } else { all_count - 1 } } else { - amyuni_count + plug_in_count } } }; - if to_plug_out_count != 0 && index != -1 { + if to_plug_out_count != 0 && !plug_out_all { to_plug_out_count = 1; } + for _i in 0..to_plug_out_count { let _ = plug_monitor_(false); } diff --git a/vcpkg.json b/vcpkg.json index f1d7036eb5f..81484772aea 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -87,7 +87,7 @@ ] }, "overrides": [ - { "name": "ffnvcodec", "version": "11.1.5.2" }, + { "name": "ffnvcodec", "version": "12.1.14.0" }, { "name": "amd-amf", "version": "1.4.29" }, { "name": "mfx-dispatch", "version": "1.35.1" } ]