116 Commits

160 changed files with 4942 additions and 2362 deletions

View File

@@ -6,7 +6,7 @@ on:
workflow_call: workflow_call:
env: env:
FLUTTER_VERSION: "3.16.9" FLUTTER_VERSION: "3.19.6"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503

View File

@@ -33,8 +33,8 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.07.12 # vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1"
VERSION: "1.3.0" VERSION: "1.3.2"
NDK_VERSION: "r27" NDK_VERSION: "r27b"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
@@ -884,7 +884,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \
@@ -976,8 +976,11 @@ jobs:
- name: fix android for flutter 3.13 - name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: | run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml cd flutter
cd flutter/lib 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' find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk lib - name: Build rustdesk lib
@@ -1144,7 +1147,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \
@@ -1210,8 +1213,11 @@ jobs:
- name: fix android for flutter 3.13 - name: fix android for flutter 3.13
if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} if: $${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
run: | run: |
sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml cd flutter
cd flutter/lib 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' find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
- name: Build rustdesk - name: Build rustdesk
@@ -1418,7 +1424,7 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libclang-10-dev \ libclang-10-dev \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
@@ -1675,7 +1681,7 @@ jobs:
gcc \ gcc \
git \ git \
g++ \ g++ \
libappindicator3-dev \ libayatana-appindicator3-dev \
libasound2-dev \ libasound2-dev \
libclang-dev \ libclang-dev \
libdbus-1-dev \ libdbus-1-dev \

View File

@@ -18,7 +18,7 @@ env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2024.06.15 # vcpkg version: 2024.06.15
VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625"
VERSION: "1.3.0" VERSION: "1.3.2"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -262,7 +262,7 @@ jobs:
git \ git \
g++ \ g++ \
g++-multilib \ g++-multilib \
libappindicator3-dev \ libayatana-appindicator3-dev\
libasound2-dev \ libasound2-dev \
libc6-dev \ libc6-dev \
libclang-10-dev \ libclang-10-dev \

33
Cargo.lock generated
View File

@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.4.0" version = "3.4.0"
source = "git+https://github.com/rustdesk-org/arboard#a04bdb1b368a99691822c33bf0f7ed497d6a7a35" source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"core-graphics 0.23.2", "core-graphics 0.23.2",
@@ -860,6 +860,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.38" version = "0.4.38"
@@ -3045,7 +3051,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hwcodec" name = "hwcodec"
version = "0.7.0" version = "0.7.0"
source = "git+https://github.com/rustdesk-org/hwcodec#6abd1898f3a03481ed0c038507b5218d6ea94267" source = "git+https://github.com/rustdesk-org/hwcodec#f74410edec91435252b8394c38f8eeca87ad2a26"
dependencies = [ dependencies = [
"bindgen 0.59.2", "bindgen 0.59.2",
"cc", "cc",
@@ -3967,11 +3973,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cfg_aliases", "cfg_aliases 0.1.1",
"libc", "libc",
"memoffset 0.9.1", "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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@@ -5187,7 +5205,7 @@ dependencies = [
[[package]] [[package]]
name = "rdev" name = "rdev"
version = "0.5.0-2" version = "0.5.0-2"
source = "git+https://github.com/rustdesk-org/rdev#b3434caee84c92412b45a2f655a15ac5dad33488" source = "git+https://github.com/rustdesk-org/rdev#d4c1759926d693ba269e2cb8cf9f87b13e424e4e"
dependencies = [ dependencies = [
"cocoa 0.24.1", "cocoa 0.24.1",
"core-foundation 0.9.4", "core-foundation 0.9.4",
@@ -5462,7 +5480,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.3.0" version = "1.3.2"
dependencies = [ dependencies = [
"android-wakelock", "android-wakelock",
"android_logger", "android_logger",
@@ -5494,6 +5512,7 @@ dependencies = [
"flutter_rust_bridge", "flutter_rust_bridge",
"fon", "fon",
"fruitbasket", "fruitbasket",
"gtk",
"hbb_common", "hbb_common",
"hex", "hex",
"hound", "hound",
@@ -5508,6 +5527,7 @@ dependencies = [
"libpulse-simple-binding", "libpulse-simple-binding",
"mac_address", "mac_address",
"magnum-opus", "magnum-opus",
"nix 0.29.0",
"num_cpus", "num_cpus",
"objc", "objc",
"objc_id", "objc_id",
@@ -5539,6 +5559,7 @@ dependencies = [
"system_shutdown", "system_shutdown",
"tao", "tao",
"tauri-winrt-notification", "tauri-winrt-notification",
"termios",
"totp-rs", "totp-rs",
"tray-icon", "tray-icon",
"url", "url",
@@ -5559,7 +5580,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.3.0" version = "1.3.2"
dependencies = [ dependencies = [
"brotli", "brotli",
"dirs 5.0.1", "dirs 5.0.1",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.3.0" version = "1.3.2"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" 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} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = {version = "2.3", optional = true} percent-encoding = {version = "2.3", optional = true}
once_cell = {version = "1.18", 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] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br> <img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Servers</a> • <a href="#public-servers">Servers</a> •
<a href="#raw-steps-to-build">Build</a> • <a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> • <a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Structure</a> • <a href="#file-structure">Structure</a> •
@@ -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) ![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) ![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)

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.3.0 version: 1.3.2
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.3.0 version: 1.3.2
exec: usr/lib/rustdesk/rustdesk exec: usr/lib/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -287,7 +287,8 @@ Version: %s
Architecture: %s Architecture: %s
Maintainer: rustdesk <info@rustdesk.com> Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com 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. Description: A remote control software.
""" % (version, get_deb_arch(), get_deb_extra_depends()) """ % (version, get_deb_arch(), get_deb_extra_depends())
@@ -330,8 +331,6 @@ def build_flutter_deb(version, features):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') '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( system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2( system2(
@@ -375,8 +374,6 @@ def build_deb_from_folder(version, binary_folder):
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2( system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') '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( system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")

View File

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

32
docs/CONTRIBUTING-ZH.md Normal file
View File

@@ -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) 进行交流。

View File

@@ -8,7 +8,7 @@
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br> [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
</p> </p>
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) [![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) ![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) [**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) | [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) | [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/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/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[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/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 - **[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/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)**: 平台服务相关代码 - **[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代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
## 截图 ## 截图

View File

@@ -17,7 +17,7 @@
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz", "url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.2/Linux-PAM-1.3.2.tar.xz",
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db" "sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
} }
] ]

View File

@@ -302,6 +302,7 @@ prebuild)
sed \ sed \
-i \ -i \
-e 's/extended_text: .*/extended_text: 11.1.0/' \
-e 's/uni_links_desktop/#uni_links_desktop/g' \ -e 's/uni_links_desktop/#uni_links_desktop/g' \
flutter/pubspec.yaml flutter/pubspec.yaml

View File

@@ -50,6 +50,9 @@ final isLinux = isLinux_;
final isDesktop = isDesktop_; final isDesktop = isDesktop_;
final isWeb = isWeb_; final isWeb = isWeb_;
final isWebDesktop = isWebDesktop_; final isWebDesktop = isWebDesktop_;
final isWebOnWindows = isWebOnWindows_;
final isWebOnLinux = isWebOnLinux_;
final isWebOnMacOs = isWebOnMacOS_;
var isMobile = isAndroid || isIOS; var isMobile = isAndroid || isIOS;
var version = ''; var version = '';
int androidVersion = 0; int androidVersion = 0;
@@ -347,6 +350,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 224, 224, 224), hoverColor: Color.fromARGB(255, 224, 224, 224),
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
dialogBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white,
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
elevation: 15, elevation: 15,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -442,6 +448,9 @@ class MyTheme {
hoverColor: Color.fromARGB(255, 45, 46, 53), hoverColor: Color.fromARGB(255, 45, 46, 53),
scaffoldBackgroundColor: Color(0xFF18191E), scaffoldBackgroundColor: Color(0xFF18191E),
dialogBackgroundColor: Color(0xFF18191E), dialogBackgroundColor: Color(0xFF18191E),
appBarTheme: AppBarTheme(
shadowColor: Colors.transparent,
),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
elevation: 15, elevation: 15,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -547,7 +556,7 @@ class MyTheme {
static void changeDarkMode(ThemeMode mode) async { static void changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode); Get.changeThemeMode(mode);
if (desktopType == DesktopType.main || isAndroid || isIOS) { if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
if (mode == ThemeMode.system) { if (mode == ThemeMode.system) {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: defaultOptionTheme); key: kCommConfKeyTheme, value: defaultOptionTheme);
@@ -555,7 +564,7 @@ class MyTheme {
await bind.mainSetLocalOption( await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString()); 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. // Synchronize the window theme of the system.
updateSystemWindowTheme(); updateSystemWindowTheme();
} }
@@ -3145,6 +3154,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs, importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
String? text) { String? text) {
text = text?.trim();
if (text != null && text.isNotEmpty) { if (text != null && text.isNotEmpty) {
try { try {
final sc = ServerConfig.decode(text); final sc = ServerConfig.decode(text);

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/platform_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 'package:url_launcher/url_launcher_string.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -61,15 +62,16 @@ class _AddressBookState extends State<AddressBook> {
retry: null, // remove retry retry: null, // remove retry
close: () => gFFI.abModel.currentAbPushError.value = ''), close: () => gFFI.abModel.currentAbPushError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildAddressBookDesktop() ? _buildAddressBookPortrait()
: _buildAddressBookMobile()) : _buildAddressBookLandscape()),
),
], ],
); );
} }
}); });
Widget _buildAddressBookDesktop() { Widget _buildAddressBookLandscape() {
return Row( return Row(
children: [ children: [
Offstage( Offstage(
@@ -106,7 +108,7 @@ class _AddressBookState extends State<AddressBook> {
); );
} }
Widget _buildAddressBookMobile() { Widget _buildAddressBookPortrait() {
const padding = 8.0; const padding = 8.0;
return Column( return Column(
children: [ children: [
@@ -239,14 +241,15 @@ class _AddressBookState extends State<AddressBook> {
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
} }
}, },
customButton: Container( customButton: Obx(() => Container(
height: isDesktop ? 48 : 40, height: stateGlobal.isPortrait.isFalse ? 48 : 40,
child: Row(children: [ child: Row(children: [
Expanded( Expanded(
child: buildItem(gFFI.abModel.currentName.value, button: true)), child:
Icon(Icons.arrow_drop_down), buildItem(gFFI.abModel.currentName.value, button: true)),
]), Icon(Icons.arrow_drop_down),
), ]),
)),
underline: Container( underline: Container(
height: 0.7, height: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
@@ -335,8 +338,8 @@ class _AddressBookState extends State<AddressBook> {
showActionMenu: editPermission); showActionMenu: editPermission);
} }
final gridView = DynamicGridView.builder( gridView(bool isPortrait) => DynamicGridView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
gridDelegate: SliverGridDelegateWithWrapping(), gridDelegate: SliverGridDelegateWithWrapping(),
itemCount: tags.length, itemCount: tags.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
@@ -344,9 +347,9 @@ class _AddressBookState extends State<AddressBook> {
return tagBuilder(e); return tagBuilder(e);
}); });
final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? gridView ? gridView(false)
: LimitedBox(maxHeight: maxHeight, child: gridView); : LimitedBox(maxHeight: maxHeight, child: gridView(true)));
}); });
} }
@@ -356,7 +359,6 @@ class _AddressBookState extends State<AddressBook> {
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: AddressBookPeersView( child: AddressBookPeersView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.abModel.currentAbPeers,
)), )),
); );
} }
@@ -506,20 +508,21 @@ class _AddressBookState extends State<AddressBook> {
double marginBottom = 4; double marginBottom = 4;
row({required Widget lable, required Widget input}) { row({required Widget lable, required Widget input}) {
return Row( makeChild(bool isPortrait) => Row(
children: [ children: [
!isMobile !isPortrait
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: lable.marginOnly(right: 10)) child: lable.marginOnly(right: 10))
: SizedBox.shrink(), : SizedBox.shrink(),
Expanded( Expanded(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200), constraints: const BoxConstraints(minWidth: 200),
child: input), child: input),
), ),
], ],
).marginOnly(bottom: !isMobile ? 8 : 0); ).marginOnly(bottom: !isPortrait ? 8 : 0);
return Obx(() => makeChild(stateGlobal.isPortrait.isTrue));
} }
return CustomAlertDialog( return CustomAlertDialog(
@@ -542,23 +545,28 @@ class _AddressBookState extends State<AddressBook> {
), ),
], ],
), ),
input: TextField( input: Obx(() => TextField(
controller: idController, controller: idController,
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('ID'), labelText: stateGlobal.isPortrait.isFalse
errorText: errorMsg, ? null
errorMaxLines: 5), : translate('ID'),
)), errorText: errorMsg,
errorMaxLines: 5),
))),
row( row(
lable: Text( lable: Text(
translate('Alias'), translate('Alias'),
style: style, style: style,
), ),
input: TextField( input: Obx(() => TextField(
controller: aliasController, controller: aliasController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: !isMobile ? null : translate('Alias'), labelText: stateGlobal.isPortrait.isFalse
? null
: translate('Alias'),
),
)), )),
), ),
if (isCurrentAbShared) if (isCurrentAbShared)
@@ -567,22 +575,26 @@ class _AddressBookState extends State<AddressBook> {
translate('Password'), translate('Password'),
style: style, style: style,
), ),
input: TextField( input: Obx(
controller: passwordController, () => TextField(
obscureText: !passwordVisible, controller: passwordController,
decoration: InputDecoration( obscureText: !passwordVisible,
labelText: !isMobile ? null : translate('Password'), decoration: InputDecoration(
suffixIcon: IconButton( labelText: stateGlobal.isPortrait.isFalse
icon: Icon( ? null
passwordVisible : translate('Password'),
? Icons.visibility suffixIcon: IconButton(
: Icons.visibility_off, icon: Icon(
color: MyTheme.lightTheme.primaryColor), passwordVisible
onPressed: () { ? Icons.visibility
setState(() { : Icons.visibility_off,
passwordVisible = !passwordVisible; color: MyTheme.lightTheme.primaryColor),
}); onPressed: () {
}, setState(() {
passwordVisible = !passwordVisible;
});
},
),
), ),
), ),
)), )),

View File

@@ -189,7 +189,7 @@ class AutocompletePeerTileState extends State<AutocompletePeerTile> {
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: widget.peer.tags.isNotEmpty : widget.peer.tags.isNotEmpty
? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}'

View File

@@ -14,7 +14,11 @@ class UppercaseValidationRule extends ValidationRule {
String get name => translate('uppercase'); String get name => translate('uppercase');
@override @override
bool validate(String value) { 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 @override
bool validate(String value) { 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;
});
} }
} }

View File

@@ -10,6 +10,7 @@ import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_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:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@@ -380,6 +381,7 @@ class DialogTextField extends StatelessWidget {
final FocusNode? focusNode; final FocusNode? focusNode;
final TextInputType? keyboardType; final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
static const kUsernameTitle = 'Username'; static const kUsernameTitle = 'Username';
static const kUsernameIcon = Icon(Icons.account_circle_outlined); static const kUsernameIcon = Icon(Icons.account_circle_outlined);
@@ -397,6 +399,7 @@ class DialogTextField extends StatelessWidget {
this.hintText, this.hintText,
this.keyboardType, this.keyboardType,
this.inputFormatters, this.inputFormatters,
this.maxLength,
required this.title, required this.title,
required this.controller}) required this.controller})
: super(key: key); : super(key: key);
@@ -423,6 +426,7 @@ class DialogTextField extends StatelessWidget {
obscureText: obscureText, obscureText: obscureText,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,
maxLength: maxLength,
), ),
), ),
], ],
@@ -680,6 +684,7 @@ class PasswordWidget extends StatefulWidget {
this.hintText, this.hintText,
this.errorText, this.errorText,
this.title, this.title,
this.maxLength,
}) : super(key: key); }) : super(key: key);
final TextEditingController controller; final TextEditingController controller;
@@ -688,6 +693,7 @@ class PasswordWidget extends StatefulWidget {
final String? hintText; final String? hintText;
final String? errorText; final String? errorText;
final String? title; final String? title;
final int? maxLength;
@override @override
State<PasswordWidget> createState() => _PasswordWidgetState(); State<PasswordWidget> createState() => _PasswordWidgetState();
@@ -750,6 +756,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
obscureText: !_passwordVisible, obscureText: !_passwordVisible,
errorText: widget.errorText, errorText: widget.errorText,
focusNode: _focusNode, focusNode: _focusNode,
maxLength: widget.maxLength,
); );
} }
} }
@@ -1123,7 +1130,7 @@ void showRequestElevationDialog(
errorText: errPwd.isEmpty ? null : errPwd.value, errorText: errPwd.isEmpty ? null : errPwd.value,
), ),
], ],
).marginOnly(left: (isDesktop || isWebDesktop) ? 35 : 0), ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
).marginOnly(top: 10), ).marginOnly(top: 10),
], ],
), ),
@@ -2244,6 +2251,7 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
final confirmController = TextEditingController(text: oldPin); final confirmController = TextEditingController(text: oldPin);
String? pinErrorText; String? pinErrorText;
String? confirmationErrorText; String? confirmationErrorText;
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() async { submit() async {
pinErrorText = null; pinErrorText = null;
@@ -2277,12 +2285,14 @@ void changeUnlockPinDialog(String oldPin, Function() callback) {
controller: pinController, controller: pinController,
obscureText: true, obscureText: true,
errorText: pinErrorText, errorText: pinErrorText,
maxLength: maxLength,
), ),
DialogTextField( DialogTextField(
title: translate('Confirmation'), title: translate('Confirmation'),
controller: confirmController, controller: confirmController,
obscureText: true, obscureText: true,
errorText: confirmationErrorText, errorText: confirmationErrorText,
maxLength: maxLength,
) )
], ],
).marginOnly(bottom: 12), ).marginOnly(bottom: 12),

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/common/widgets/login.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
@@ -45,15 +46,15 @@ class _MyGroupState extends State<MyGroup> {
retry: null, retry: null,
close: () => gFFI.groupModel.groupLoadError.value = ''), close: () => gFFI.groupModel.groupLoadError.value = ''),
Expanded( Expanded(
child: (isDesktop || isWebDesktop) child: Obx(() => stateGlobal.isPortrait.isTrue
? _buildDesktop() ? _buildPortrait()
: _buildMobile()) : _buildLandscape())),
], ],
); );
}); });
} }
Widget _buildDesktop() { Widget _buildLandscape() {
return Row( return Row(
children: [ children: [
Container( Container(
@@ -82,14 +83,14 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.groupModel.peers)), )),
) )
], ],
); );
} }
Widget _buildMobile() { Widget _buildPortrait() {
return Column( return Column(
children: [ children: [
Container( Container(
@@ -114,8 +115,8 @@ class _MyGroupState extends State<MyGroup> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: MyGroupPeerView( child: MyGroupPeerView(
menuPadding: widget.menuPadding, menuPadding: widget.menuPadding,
getInitPeers: () => gFFI.groupModel.peers)), )),
) )
], ],
); );
@@ -159,14 +160,14 @@ class _MyGroupState extends State<MyGroup> {
} }
return true; return true;
}).toList(); }).toList();
final listView = ListView.builder( listView(bool isPortrait) => ListView.builder(
shrinkWrap: isMobile, shrinkWrap: isPortrait,
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => _buildUserItem(items[index]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return (isDesktop || isWebDesktop) return Obx(() => stateGlobal.isPortrait.isFalse
? listView ? listView(false)
: LimitedBox(maxHeight: maxHeight, child: listView); : LimitedBox(maxHeight: maxHeight, child: listView(true)));
}); });
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/peer_tab_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:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -53,14 +54,11 @@ class _PeerCardState extends State<_PeerCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (isDesktop || isWebDesktop) { return Obx(() =>
return _buildDesktop(); stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape());
} else {
return _buildMobile();
}
} }
Widget _buildMobile() { Widget _buildPortrait() {
final peer = super.widget.peer; final peer = super.widget.peer;
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
return Card( return Card(
@@ -87,7 +85,7 @@ class _PeerCardState extends State<_PeerCard>
)); ));
} }
Widget _buildDesktop() { Widget _buildLandscape() {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer; final peer = super.widget.peer;
var deco = Rx<BoxDecoration?>( var deco = Rx<BoxDecoration?>(
@@ -140,13 +138,13 @@ class _PeerCardState extends State<_PeerCard>
final greyStyle = TextStyle( final greyStyle = TextStyle(
fontSize: 11, fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
final child = Row( makeChild(bool isPortrait) => Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: str2color('${peer.id}${peer.platform}', 0x7f), color: str2color('${peer.id}${peer.platform}', 0x7f),
borderRadius: isMobile borderRadius: isPortrait
? BorderRadius.circular(_tileRadius) ? BorderRadius.circular(_tileRadius)
: BorderRadius.only( : BorderRadius.only(
topLeft: Radius.circular(_tileRadius), topLeft: Radius.circular(_tileRadius),
@@ -154,11 +152,11 @@ class _PeerCardState extends State<_PeerCard>
), ),
), ),
alignment: Alignment.center, alignment: Alignment.center,
width: isMobile ? 50 : 42, width: isPortrait ? 50 : 42,
height: isMobile ? 50 : null, height: isPortrait ? 50 : null,
child: Stack( child: Stack(
children: [ children: [
getPlatformImage(peer.platform, size: isMobile ? 38 : 30) getPlatformImage(peer.platform, size: isPortrait ? 38 : 30)
.paddingAll(6), .paddingAll(6),
if (_shouldBuildPasswordIcon(peer)) if (_shouldBuildPasswordIcon(peer))
Positioned( Positioned(
@@ -183,19 +181,19 @@ class _PeerCardState extends State<_PeerCard>
child: Column( child: Column(
children: [ children: [
Row(children: [ Row(children: [
getOnline(isMobile ? 4 : 8, peer.online), getOnline(isPortrait ? 4 : 8, peer.online),
Expanded( Expanded(
child: Text( child: Text(
peer.alias.isEmpty ? formatID(peer.id) : peer.alias, peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).marginOnly(top: isMobile ? 0 : 2), ]).marginOnly(top: isPortrait ? 0 : 2),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
name, name,
style: isMobile ? null : greyStyle, style: isPortrait ? null : greyStyle,
textAlign: TextAlign.start, textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -203,9 +201,9 @@ class _PeerCardState extends State<_PeerCard>
], ],
).marginOnly(top: 2), ).marginOnly(top: 2),
), ),
isMobile isPortrait
? checkBoxOrActionMoreMobile(peer) ? checkBoxOrActionMorePortrait(peer)
: checkBoxOrActionMoreDesktop(peer, isTile: true), : checkBoxOrActionMoreLandscape(peer, isTile: true),
], ],
).paddingOnly(left: 10.0, top: 3.0), ).paddingOnly(left: 10.0, top: 3.0),
), ),
@@ -216,28 +214,27 @@ class _PeerCardState extends State<_PeerCard>
.map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .map((e) => gFFI.abModel.getCurrentAbTagColor(e))
.toList(); .toList();
return Tooltip( return Tooltip(
message: isMobile message: !(isDesktop || isWebDesktop)
? '' ? ''
: peer.tags.isNotEmpty : peer.tags.isNotEmpty
? '${translate('Tags')}: ${peer.tags.join(', ')}' ? '${translate('Tags')}: ${peer.tags.join(', ')}'
: '', : '',
child: Stack(children: [ child: Stack(children: [
deco == null Obx(() => deco == null
? child ? makeChild(stateGlobal.isPortrait.isTrue)
: Obx( : Container(
() => Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
child: child, child: makeChild(stateGlobal.isPortrait.isTrue),
), ),
), ),
if (colors.isNotEmpty) if (colors.isNotEmpty)
Positioned( Obx(()=> Positioned(
top: 2, top: 2,
right: isMobile ? 20 : 10, right: stateGlobal.isPortrait.isTrue ? 20 : 10,
child: CustomPaint( child: CustomPaint(
painter: TagPainter(radius: 3, colors: colors), painter: TagPainter(radius: 3, colors: colors),
), ),
) ))
]), ]),
); );
} }
@@ -253,6 +250,9 @@ class _PeerCardState extends State<_PeerCard>
color: Colors.transparent, color: Colors.transparent,
elevation: 0, elevation: 0,
margin: EdgeInsets.zero, 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( child: Obx(
() => Container( () => Container(
foregroundDecoration: deco.value, foregroundDecoration: deco.value,
@@ -316,7 +316,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).paddingSymmetric(vertical: 8)), ]).paddingSymmetric(vertical: 8)),
checkBoxOrActionMoreDesktop(peer, isTile: false), checkBoxOrActionMoreLandscape(peer, isTile: false),
], ],
).paddingSymmetric(horizontal: 12.0), ).paddingSymmetric(horizontal: 12.0),
) )
@@ -362,7 +362,7 @@ class _PeerCardState extends State<_PeerCard>
} }
} }
Widget checkBoxOrActionMoreMobile(Peer peer) { Widget checkBoxOrActionMorePortrait(Peer peer) {
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -390,7 +390,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 PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id); final selected = peerTabModel.isPeerSelected(peer.id);
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
@@ -1203,6 +1203,7 @@ class MyGroupPeerCard extends BasePeerCard {
} }
void _rdpDialog(String id) async { void _rdpDialog(String id) async {
final maxLength = bind.mainMaxEncryptLen();
final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port); final portController = TextEditingController(text: port);
@@ -1257,9 +1258,9 @@ void _rdpDialog(String id) async {
), ),
], ],
).marginOnly(bottom: isDesktop ? 8 : 0), ).marginOnly(bottom: isDesktop ? 8 : 0),
Row( Obx(() => Row(
children: [ children: [
(isDesktop || isWebDesktop) stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
@@ -1270,17 +1271,17 @@ void _rdpDialog(String id) async {
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop) labelText: isDesktop
? null ? null
: translate('Username')), : translate('Username')),
controller: userController, controller: userController,
), ),
), ),
], ],
).marginOnly(bottom: (isDesktop || isWebDesktop) ? 8 : 0), ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)),
Row( Obx(() => Row(
children: [ children: [
(isDesktop || isWebDesktop) stateGlobal.isPortrait.isFalse
? ConstrainedBox( ? ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140), constraints: const BoxConstraints(minWidth: 140),
child: Text( child: Text(
@@ -1291,8 +1292,9 @@ void _rdpDialog(String id) async {
Expanded( Expanded(
child: Obx(() => TextField( child: Obx(() => TextField(
obscureText: secure.value, obscureText: secure.value,
maxLength: maxLength,
decoration: InputDecoration( decoration: InputDecoration(
labelText: (isDesktop || isWebDesktop) labelText: isDesktop
? null ? null
: translate('Password'), : translate('Password'),
suffixIcon: IconButton( suffixIcon: IconButton(
@@ -1304,7 +1306,7 @@ void _rdpDialog(String id) async {
)), )),
), ),
], ],
) ))
], ],
), ),
), ),

View File

@@ -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_model.dart';
import 'package:flutter_hbb/models/peer_tab_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:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -107,33 +108,33 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
Widget selectionWrap(Widget widget) { Widget selectionWrap(Widget widget) {
return model.multiSelectionMode ? createMultiSelectionBar() : widget; return model.multiSelectionMode ? createMultiSelectionBar(model) : widget;
} }
return Column( return Column(
textBaseline: TextBaseline.ideographic, textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Obx(() => SizedBox(
height: 32, height: 32,
child: Container( child: Container(
padding: (isDesktop || isWebDesktop) padding: stateGlobal.isPortrait.isTrue
? null ? EdgeInsets.symmetric(horizontal: 2)
: EdgeInsets.symmetric(horizontal: 2), : null,
child: selectionWrap(Row( child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
child: child: visibleContextMenuListener(
visibleContextMenuListener(_createSwitchBar(context))), _createSwitchBar(context))),
if (isMobile) if (stateGlobal.isPortrait.isTrue)
..._mobileRightActions(context) ..._portraitRightActions(context)
else else
..._desktopRightActions(context) ..._landscapeRightActions(context)
], ],
)), )),
), ),
).paddingOnly(right: (isDesktop || isWebDesktop) ? 12 : 0), ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)),
_createPeersView(), _createPeersView(),
], ],
); );
@@ -299,7 +300,7 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget visibleContextMenuListener(Widget child) { Widget visibleContextMenuListener(Widget child) {
if (isMobile) { if (!(isDesktop || isWebDesktop)) {
return GestureDetector( return GestureDetector(
onLongPressDown: (e) { onLongPressDown: (e) {
final x = e.globalPosition.dx; final x = e.globalPosition.dx;
@@ -361,8 +362,7 @@ class _PeerTabPageState extends State<PeerTabPage>
.toList()); .toList());
} }
Widget createMultiSelectionBar() { Widget createMultiSelectionBar(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -380,7 +380,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Row( Row(
children: [ children: [
selectionCount(model.selectedPeers.length), selectionCount(model.selectedPeers.length),
selectAll(), selectAll(model),
closeSelection(), closeSelection(),
], ],
) )
@@ -456,7 +456,7 @@ class _PeerTabPageState extends State<PeerTabPage>
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), 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<PeerTabPage>
model.setMultiSelectionMode(false); model.setMultiSelectionMode(false);
}, },
child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), 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<PeerTabPage>
}); });
}, },
child: Icon(Icons.tag)) child: Icon(Icons.tag))
.marginOnly(left: isMobile ? 11 : 6), .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6),
); );
} }
@@ -511,8 +511,7 @@ class _PeerTabPageState extends State<PeerTabPage>
); );
} }
Widget selectAll() { Widget selectAll(PeerTabModel model) {
final model = Provider.of<PeerTabModel>(context);
return Offstage( return Offstage(
offstage: offstage:
model.selectedPeers.length >= model.currentTabCachedPeers.length, model.selectedPeers.length >= model.currentTabCachedPeers.length,
@@ -556,10 +555,10 @@ class _PeerTabPageState extends State<PeerTabPage>
}); });
} }
List<Widget> _desktopRightActions(BuildContext context) { List<Widget> _landscapeRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return [ return [
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), const PeerSearchBar().marginOnly(right: 13),
_createRefresh( _createRefresh(
index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading),
_createRefresh( _createRefresh(
@@ -580,7 +579,7 @@ class _PeerTabPageState extends State<PeerTabPage>
]; ];
} }
List<Widget> _mobileRightActions(BuildContext context) { List<Widget> _portraitRightActions(BuildContext context) {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftIconSize = Theme.of(context).iconTheme.size ?? 24;
@@ -701,13 +700,13 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
baseOffset: 0, baseOffset: 0,
extentOffset: peerSearchTextController.value.text.length); extentOffset: peerSearchTextController.value.text.length);
}); });
return Container( return Obx(() => Container(
width: isMobile ? 120 : 140, width: stateGlobal.isPortrait.isTrue ? 120 : 140,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Obx(() => Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: Row( child: Row(
@@ -768,8 +767,8 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
), ),
) )
], ],
)), ),
); ));
} }
} }

View File

@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.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:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
@@ -41,6 +43,14 @@ class LoadEvent {
static const String group = 'load_group_peers'; 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 /// for peer search text, global obs value
final peerSearchText = "".obs; final peerSearchText = "".obs;
@@ -88,6 +98,7 @@ class _PeersViewState extends State<_PeersView>
var _lastChangeTime = DateTime.now(); var _lastChangeTime = DateTime.now();
var _lastQueryPeers = <String>{}; var _lastQueryPeers = <String>{};
var _lastQueryTime = DateTime.now(); var _lastQueryTime = DateTime.now();
var _lastWindowRestoreTime = DateTime.now();
var _queryCount = 0; var _queryCount = 0;
var _exit = false; var _exit = false;
bool _isActive = true; bool _isActive = true;
@@ -116,11 +127,38 @@ class _PeersViewState extends State<_PeersView>
@override @override
void onWindowFocus() { void onWindowFocus() {
_queryCount = 0; _queryCount = 0;
_isActive = true;
}
@override
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 @override
void onWindowMinimize() { void onWindowMinimize() {
_queryCount = _maxQueryCount; // Window minimize also triggers `onWindowBlur()`.
} }
// This function is required for mobile. // This function is required for mobile.
@@ -128,7 +166,7 @@ class _PeersViewState extends State<_PeersView>
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
if (isDesktop) return; if (isDesktop || isWebDesktop) return;
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
_isActive = true; _isActive = true;
_queryCount = 0; _queryCount = 0;
@@ -139,8 +177,11 @@ class _PeersViewState extends State<_PeersView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<Peers>( // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6.
create: (context) => widget.peers, // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak.
// Simple demo can reproduce this issue.
return ChangeNotifierProvider<Peers>.value(
value: widget.peers,
child: Consumer<Peers>(builder: (context, peers, child) { child: Consumer<Peers>(builder: (context, peers, child) {
if (peers.peers.isEmpty) { if (peers.peers.isEmpty) {
gFFI.peerTabModel.setCurrentTabCachedPeers([]); gFFI.peerTabModel.setCurrentTabCachedPeers([]);
@@ -194,7 +235,7 @@ class _PeersViewState extends State<_PeersView>
var peers = snapshot.data!; var peers = snapshot.data!;
if (peers.length > 1000) peers = peers.sublist(0, 1000); if (peers.length > 1000) peers = peers.sublist(0, 1000);
gFFI.peerTabModel.setCurrentTabCachedPeers(peers); gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
buildOnePeer(Peer peer) { buildOnePeer(Peer peer, bool isPortrait) {
final visibilityChild = VisibilityDetector( final visibilityChild = VisibilityDetector(
key: ValueKey(_cardId(peer.id)), key: ValueKey(_cardId(peer.id)),
onVisibilityChanged: onVisibilityChanged, onVisibilityChanged: onVisibilityChanged,
@@ -206,7 +247,7 @@ class _PeersViewState extends State<_PeersView>
// No need to listen the currentTab change event. // No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event, // Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView(). // and the peers change event will trigger _buildPeersView().
return (isDesktop || isWebDesktop) return !isPortrait
? Obx(() => peerCardUiType.value == PeerUiType.list ? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild) ? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid : peerCardUiType.value == PeerUiType.grid
@@ -217,44 +258,45 @@ class _PeersViewState extends State<_PeersView>
: Container(child: visibilityChild); : Container(child: visibilityChild);
} }
final Widget child; // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6.
if (isMobile) { // Continious rebuilds of `ListView.builder` will cause memory leak.
child = ListView.builder( // Simple demo can reproduce this issue.
itemCount: peers.length, final Widget child = Obx(() => stateGlobal.isPortrait.isTrue
itemBuilder: (BuildContext context, int index) { ? ListView.builder(
return buildOnePeer(peers[index]).marginOnly( itemCount: peers.length,
top: index == 0 ? 0 : space / 2, bottom: space / 2); itemBuilder: (BuildContext context, int index) {
}, return buildOnePeer(peers[index], true).marginOnly(
); top: index == 0 ? 0 : space / 2, bottom: space / 2);
} else { },
child = Obx(() => peerCardUiType.value == PeerUiType.list )
? DesktopScrollWrapper( : peerCardUiType.value == PeerUiType.list
scrollController: _scrollController, ? DesktopScrollWrapper(
child: ListView.builder( scrollController: _scrollController,
controller: _scrollController, child: ListView.builder(
physics: DraggableNeverScrollableScrollPhysics(), controller: _scrollController,
itemCount: peers.length, physics: DraggableNeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) { itemCount: peers.length,
return buildOnePeer(peers[index]).marginOnly( itemBuilder: (BuildContext context, int index) {
right: space, return buildOnePeer(peers[index], false)
top: index == 0 ? 0 : space / 2, .marginOnly(
bottom: space / 2); right: space,
}), top: index == 0 ? 0 : space / 2,
) bottom: space / 2);
: DesktopScrollWrapper( }),
scrollController: _scrollController, )
child: DynamicGridView.builder( : DesktopScrollWrapper(
controller: _scrollController, scrollController: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(), child: DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping( controller: _scrollController,
mainAxisSpacing: space / 2, physics: DraggableNeverScrollableScrollPhysics(),
crossAxisSpacing: space), gridDelegate: SliverGridDelegateWithWrapping(
itemCount: peers.length, mainAxisSpacing: space / 2,
itemBuilder: (BuildContext context, int index) { crossAxisSpacing: space),
return buildOnePeer(peers[index]); itemCount: peers.length,
}), itemBuilder: (BuildContext context, int index) {
)); return buildOnePeer(peers[index], false);
} }),
));
if (updateEvent == UpdateEvent.load) { if (updateEvent == UpdateEvent.load) {
_curPeers.clear(); _curPeers.clear();
@@ -371,28 +413,39 @@ class _PeersViewState extends State<_PeersView>
} }
abstract class BasePeersView extends StatelessWidget { abstract class BasePeersView extends StatelessWidget {
final String name; final PeerTabIndex peerTabIndex;
final String loadEvent;
final PeerFilter? peerFilter; final PeerFilter? peerFilter;
final PeerCardBuilder peerCardBuilder; final PeerCardBuilder peerCardBuilder;
final GetInitPeers? getInitPeers;
const BasePeersView({ const BasePeersView({
Key? key, Key? key,
required this.name, required this.peerTabIndex,
required this.loadEvent,
this.peerFilter, this.peerFilter,
required this.peerCardBuilder, required this.peerCardBuilder,
required this.getInitPeers,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { 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( return _PeersView(
peers: peers: peers, peerFilter: peerFilter, peerCardBuilder: peerCardBuilder);
Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers),
peerFilter: peerFilter,
peerCardBuilder: peerCardBuilder);
} }
} }
@@ -401,13 +454,11 @@ class RecentPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'recent peer', peerTabIndex: PeerTabIndex.recent,
loadEvent: LoadEvent.recent,
peerCardBuilder: (Peer peer) => RecentPeerCard( peerCardBuilder: (Peer peer) => RecentPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -423,13 +474,11 @@ class FavoritePeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'favorite peer', peerTabIndex: PeerTabIndex.fav,
loadEvent: LoadEvent.favorite,
peerCardBuilder: (Peer peer) => FavoritePeerCard( peerCardBuilder: (Peer peer) => FavoritePeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -445,13 +494,11 @@ class DiscoveredPeersView extends BasePeersView {
{Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
: super( : super(
key: key, key: key,
name: 'discovered peer', peerTabIndex: PeerTabIndex.lan,
loadEvent: LoadEvent.lan,
peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peerCardBuilder: (Peer peer) => DiscoveredPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: null,
); );
@override @override
@@ -464,21 +511,16 @@ class DiscoveredPeersView extends BasePeersView {
class AddressBookPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView {
AddressBookPeersView( AddressBookPeersView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required GetInitPeers getInitPeers})
: super( : super(
key: key, key: key,
name: 'address book peer', peerTabIndex: PeerTabIndex.ab,
loadEvent: LoadEvent.addressBook,
peerFilter: (Peer peer) => peerFilter: (Peer peer) =>
_hitTag(gFFI.abModel.selectedTags, peer.tags), _hitTag(gFFI.abModel.selectedTags, peer.tags),
peerCardBuilder: (Peer peer) => AddressBookPeerCard( peerCardBuilder: (Peer peer) => AddressBookPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: getInitPeers,
); );
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) { static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
@@ -505,20 +547,15 @@ class AddressBookPeersView extends BasePeersView {
class MyGroupPeerView extends BasePeersView { class MyGroupPeerView extends BasePeersView {
MyGroupPeerView( MyGroupPeerView(
{Key? key, {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController})
EdgeInsets? menuPadding,
ScrollController? scrollController,
required GetInitPeers getInitPeers})
: super( : super(
key: key, key: key,
name: 'group peer', peerTabIndex: PeerTabIndex.group,
loadEvent: LoadEvent.group,
peerFilter: filter, peerFilter: filter,
peerCardBuilder: (Peer peer) => MyGroupPeerCard( peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer, peer: peer,
menuPadding: menuPadding, menuPadding: menuPadding,
), ),
getInitPeers: getInitPeers,
); );
static bool filter(Peer peer) { static bool filter(Peer peer) {

View File

@@ -27,6 +27,10 @@ class RawKeyFocusScope extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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( return FocusScope(
autofocus: true, autofocus: true,
child: Focus( child: Focus(
@@ -34,8 +38,14 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true, canRequestFocus: true,
focusNode: focusNode, focusNode: focusNode,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
onKey: (FocusNode data, RawKeyEvent e) => onKey: useRawKeyEvents
inputModel.handleRawKeyEvent(e), ? (FocusNode data, RawKeyEvent event) =>
inputModel.handleRawKeyEvent(event)
: null,
onKeyEvent: useRawKeyEvents
? null
: (FocusNode node, KeyEvent event) =>
inputModel.handleKeyEvent(event),
child: child)); child: child));
} }
} }
@@ -233,7 +243,7 @@ class _RawTouchGestureDetectorRegionState
if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords(); ffi.cursorModel.trySetRemoteWindowCoords();
} }
// Workaround for the issue that the first pan event is sent a long time after the start event. // 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) { if (lastDeviceKind != PointerDeviceKind.touch) {
return; return;
} }
if (isDesktop) { if (isDesktop || isWebDesktop) {
ffi.cursorModel.clearRemoteWindowCoords(); ffi.cursorModel.clearRemoteWindowCoords();
} }
inputModel.sendMouse('up', MouseButtons.left); inputModel.sendMouse('up', MouseButtons.left);

View File

@@ -32,6 +32,7 @@ const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android"; const String kPeerPlatformAndroid = "Android";
const String kPeerPlatformWebDesktop = "WebDesktop";
const double kScrollbarThickness = 12.0; const double kScrollbarThickness = 12.0;
@@ -569,3 +570,5 @@ enum WindowsTarget {
extension WindowsTargetExt on int { extension WindowsTargetExt on int {
WindowsTarget get windowsVersion => getWindowsTarget(this); WindowsTarget get windowsVersion => getWindowsTarget(this);
} }
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';

View File

@@ -664,9 +664,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
void initState() { void initState() {
super.initState(); super.initState();
if (!bind.isCustomClient()) { if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
(Map<String, dynamic> evt) async {
if (evt['url'] is String) {
setState(() {
updateUrl = evt['url'];
});
}
});
Timer(const Duration(seconds: 1), () async { Timer(const Duration(seconds: 1), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl(); bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
}); });
} }
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async { _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
@@ -824,6 +832,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
_uniLinksSubscription?.cancel(); _uniLinksSubscription?.cancel();
Get.delete<RxBool>(tag: 'stop-service'); Get.delete<RxBool>(tag: 'stop-service');
_updateTimer?.cancel(); _updateTimer?.cancel();
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
super.dispose(); super.dispose();
} }
@@ -857,6 +869,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
// SpecialCharacterValidationRule(), // SpecialCharacterValidationRule(),
MinCharactersValidationRule(8), MinCharactersValidationRule(8),
]; ];
final maxLength = bind.mainMaxEncryptLen();
gFFI.dialogManager.show((setState, close, context) { gFFI.dialogManager.show((setState, close, context) {
submit() { submit() {
@@ -915,6 +928,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg0 = ''; errMsg0 = '';
}); });
}, },
maxLength: maxLength,
), ),
), ),
], ],
@@ -941,6 +955,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
errMsg1 = ''; errMsg1 = '';
}); });
}, },
maxLength: maxLength,
), ),
), ),
], ],

View File

@@ -61,7 +61,8 @@ class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey; final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [ static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general, SettingsTabKey.general,
if (!bind.isOutgoingOnly() && if (!isWeb &&
!bind.isOutgoingOnly() &&
!bind.isDisableSettings() && !bind.isDisableSettings() &&
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
SettingsTabKey.safety, SettingsTabKey.safety,
@@ -216,7 +217,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
width: _kTabWidth, width: _kTabWidth,
child: Column( child: Column(
children: [ children: [
_header(), _header(context),
Flexible(child: _listView(tabs: _settingTabs())), Flexible(child: _listView(tabs: _settingTabs())),
], ],
), ),
@@ -239,21 +240,40 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
); );
} }
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( return Row(
children: [ children: [
SizedBox( if (isWeb)
height: 62, IconButton(
child: Text( onPressed: () {
translate('Settings'), if (Navigator.canPop(context)) {
textAlign: TextAlign.left, Navigator.pop(context);
style: const TextStyle( }
color: _accentColor, },
fontSize: _kTitleFontSize, icon: Icon(Icons.arrow_back),
fontWeight: FontWeight.w400, ).marginOnly(left: 5),
if (isWeb)
SizedBox(
height: 62,
child: Align(
alignment: Alignment.center,
child: settingsText,
), ),
), ).marginOnly(left: 20),
).marginOnly(left: 20, top: 10), if (!isWeb)
SizedBox(
height: 62,
child: settingsText,
).marginOnly(left: 20, top: 10),
const Spacer(), const Spacer(),
], ],
); );
@@ -322,7 +342,8 @@ class _General extends StatefulWidget {
} }
class _GeneralState extends State<_General> { class _GeneralState extends State<_General> {
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service'); final RxBool serviceStop =
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs; RxBool serviceBtnEnabled = true.obs;
@override @override
@@ -334,13 +355,13 @@ class _GeneralState extends State<_General> {
physics: DraggableNeverScrollableScrollPhysics(), physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController, controller: scrollController,
children: [ children: [
service(), if (!isWeb) service(),
theme(), theme(),
_Card(title: 'Language', children: [language()]), _Card(title: 'Language', children: [language()]),
hwcodec(), if (!isWeb) hwcodec(),
audio(context), if (!isWeb) audio(context),
record(context), if (!isWeb) record(context),
WaylandCard(), if (!isWeb) WaylandCard(),
other() other()
], ],
).marginOnly(bottom: _kListViewBottomMargin)); ).marginOnly(bottom: _kListViewBottomMargin));
@@ -394,13 +415,13 @@ class _GeneralState extends State<_General> {
Widget other() { Widget other() {
final children = <Widget>[ final children = <Widget>[
if (!bind.isIncomingOnly()) if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs', _OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs, kOptionEnableConfirmClosingTabs,
isServer: false), isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
wallpaper(), if (!isWeb) wallpaper(),
if (!bind.isIncomingOnly()) ...[ if (!isWeb && !bind.isIncomingOnly()) ...[
_OptionCheckBox( _OptionCheckBox(
context, context,
'Open connection in new tab', 'Open connection in new tab',
@@ -417,18 +438,19 @@ class _GeneralState extends State<_General> {
kOptionAllowAlwaysSoftwareRender, kOptionAllowAlwaysSoftwareRender,
), ),
), ),
Tooltip( if (!isWeb)
message: translate('texture_render_tip'), Tooltip(
child: _OptionCheckBox( message: translate('texture_render_tip'),
context, child: _OptionCheckBox(
"Use texture rendering", context,
kOptionTextureRender, "Use texture rendering",
optGetter: bind.mainGetUseTextureRender, kOptionTextureRender,
optSetter: (k, v) async => optGetter: bind.mainGetUseTextureRender,
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), optSetter: (k, v) async =>
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
),
), ),
), if (!isWeb && !bind.isCustomClient())
if (!bind.isCustomClient())
_OptionCheckBox( _OptionCheckBox(
context, context,
'Check for software update on startup', '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( children.add(_OptionCheckBox(
context, 'Allow linux headless', kOptionAllowLinuxHeadless)); context, 'Allow linux headless', kOptionAllowLinuxHeadless));
} }
@@ -641,8 +663,9 @@ class _GeneralState extends State<_General> {
initialKey: currentKey, initialKey: currentKey,
onChanged: (key) async { onChanged: (key) async {
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
reloadAllWindows(); if (isWeb) reloadCurrentWindow();
bind.mainChangeLanguage(lang: key); if (!isWeb) reloadAllWindows();
if (!isWeb) bind.mainChangeLanguage(lang: key);
}, },
enabled: !isOptFixed, enabled: !isOptFixed,
).marginOnly(left: _kContentHMargin); ).marginOnly(left: _kContentHMargin);
@@ -1337,7 +1360,7 @@ class _Network extends StatefulWidget {
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled(); bool locked = !isWeb && bind.mainIsInstalled();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -1346,8 +1369,9 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
final scrollController = ScrollController(); final scrollController = ScrollController();
final hideServer = final hideServer =
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
// TODO: support web proxy
final hideProxy = final hideProxy =
bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
return DesktopScrollWrapper( return DesktopScrollWrapper(
scrollController: scrollController, scrollController: scrollController,
child: ListView( child: ListView(
@@ -1467,7 +1491,7 @@ class _DisplayState extends State<_Display> {
scrollStyle(context), scrollStyle(context),
imageQuality(context), imageQuality(context),
codec(context), codec(context),
privacyModeImpl(context), if (!isWeb) privacyModeImpl(context),
other(context), other(context),
]).marginOnly(bottom: _kListViewBottomMargin)); ]).marginOnly(bottom: _kListViewBottomMargin));
} }
@@ -1878,9 +1902,10 @@ class _AboutState extends State<_About> {
SelectionArea( SelectionArea(
child: Text('${translate('Build Date')}: $buildDate') child: Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0)), .marginSymmetric(vertical: 4.0)),
SelectionArea( if (!isWeb)
child: Text('${translate('Fingerprint')}: $fingerprint') SelectionArea(
.marginSymmetric(vertical: 4.0)), child: Text('${translate('Fingerprint')}: $fingerprint')
.marginSymmetric(vertical: 4.0)),
InkWell( InkWell(
onTap: () { onTap: () {
launchUrlString('https://rustdesk.com/privacy.html'); launchUrlString('https://rustdesk.com/privacy.html');
@@ -2487,6 +2512,7 @@ void changeSocks5Proxy() async {
: Icons.visibility))), : Icons.visibility))),
controller: pwdController, controller: pwdController,
enabled: !isOptFixed, enabled: !isOptFixed,
maxLength: bind.mainMaxEncryptLen(),
)), )),
), ),
], ],

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:extended_text/extended_text.dart';
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
import 'package:percent_indicator/percent_indicator.dart'; import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
@@ -68,7 +69,7 @@ class FileManagerPage extends StatefulWidget {
} }
class _FileManagerPageState extends State<FileManagerPage> class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none); final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _dropMaskVisible = false.obs; // TODO impl drop mask final _dropMaskVisible = false.obs; // TODO impl drop mask
@@ -102,6 +103,7 @@ class _FileManagerPageState extends State<FileManagerPage>
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id); widget.tabController.onSelected?.call(widget.id);
}); });
WidgetsBinding.instance.addObserver(this);
} }
@override @override
@@ -114,12 +116,21 @@ class _FileManagerPageState extends State<FileManagerPage>
} }
Get.delete<FFI>(tag: 'ft_${widget.id}'); Get.delete<FFI>(tag: 'ft_${widget.id}');
}); });
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
jobController.jobTable.refresh();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@@ -173,10 +184,25 @@ class _FileManagerPageState extends State<FileManagerPage>
/// transfer status list /// transfer status list
/// watch transfer status /// watch transfer status
Widget statusList() { 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: job.isRemoteToLocal ? pi : 0,
child: Icon(Icons.arrow_forward_ios, color: color),
);
}
}
statusListView(List<JobProgress> jobs) => ListView.builder( statusListView(List<JobProgress> jobs) => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = jobs[index]; final item = jobs[index];
final status = item.getStatus();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 5), padding: const EdgeInsets.only(bottom: 5),
child: generateCard( child: generateCard(
@@ -186,15 +212,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Transform.rotate( getIcon(item)
angle: item.isRemoteToLocal ? pi : 0, .marginSymmetric(horizontal: 10, vertical: 12),
child: SvgPicture.asset("assets/arrow.svg",
colorFilter: svgColor(
Theme.of(context).tabBarTheme.labelColor)),
).paddingOnly(left: 15),
const SizedBox(
width: 16.0,
),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -203,45 +222,28 @@ class _FileManagerPageState extends State<FileManagerPage>
Tooltip( Tooltip(
waitDuration: Duration(milliseconds: 500), waitDuration: Duration(milliseconds: 500),
message: item.jobName, message: item.jobName,
child: Text( child: ExtendedText(
item.fileName, item.jobName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).paddingSymmetric(vertical: 10), overflowWidget: TextOverflowWidget(
), child: Text("..."),
Text( position: TextOverflowPosition.start),
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
), ),
), ),
Offstage( Tooltip(
offstage: item.state != JobState.inProgress, waitDuration: Duration(milliseconds: 500),
child: Text( message: status,
'${translate("Speed")} ${readableFileSize(item.speed)}/s', child: Text(status,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: MyTheme.darkGray,
), )).marginOnly(top: 6),
),
), ),
Offstage( Offstage(
offstage: item.state == JobState.inProgress, offstage: item.type != JobType.transfer ||
child: Text( item.state != JobState.inProgress,
translate(
item.display(),
),
style: TextStyle(
fontSize: 12,
color: MyTheme.darkGray,
),
),
),
Offstage(
offstage: item.state != JobState.inProgress,
child: LinearPercentIndicator( child: LinearPercentIndicator(
padding: EdgeInsets.only(right: 15),
animateFromLastPercent: true, animateFromLastPercent: true,
center: Text( center: Text(
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
@@ -251,7 +253,7 @@ class _FileManagerPageState extends State<FileManagerPage>
progressColor: MyTheme.accent, progressColor: MyTheme.accent,
backgroundColor: Theme.of(context).hoverColor, backgroundColor: Theme.of(context).hoverColor,
lineHeight: kDesktopFileTransferRowHeight, lineHeight: kDesktopFileTransferRowHeight,
).paddingSymmetric(vertical: 15), ).paddingSymmetric(vertical: 8),
), ),
], ],
), ),
@@ -276,7 +278,6 @@ class _FileManagerPageState extends State<FileManagerPage>
), ),
MenuButton( MenuButton(
tooltip: translate("Delete"), tooltip: translate("Delete"),
padding: EdgeInsets.only(right: 15),
child: SvgPicture.asset( child: SvgPicture.asset(
"assets/close.svg", "assets/close.svg",
colorFilter: svgColor(Colors.white), colorFilter: svgColor(Colors.white),
@@ -289,11 +290,11 @@ class _FileManagerPageState extends State<FileManagerPage>
hoverColor: MyTheme.accent80, hoverColor: MyTheme.accent80,
), ),
], ],
), ).marginAll(12),
], ],
), ),
], ],
).paddingSymmetric(vertical: 10), ),
), ),
); );
}, },
@@ -943,6 +944,7 @@ class _FileManagerViewState extends State<FileManagerView> {
BuildContext context, ScrollController scrollController) { BuildContext context, ScrollController scrollController) {
final fd = controller.directory.value; final fd = controller.directory.value;
final entries = fd.entries; final entries = fd.entries;
Rx<Entry?> rightClickEntry = Rx(null);
return ListSearchActionListener( return ListSearchActionListener(
node: _keyboardNode, node: _keyboardNode,
@@ -1002,16 +1004,69 @@ class _FileManagerViewState extends State<FileManagerView> {
? " " ? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} "; : "${entry.lastModified().toString().replaceAll(".000", "")} ";
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0); 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("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( return Padding(
padding: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(vertical: 1),
child: Obx(() => Container( child: Obx(() => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: selectedItems.items.contains(entry) color: selectedItems.items.contains(entry)
? Theme.of(context).hoverColor ? MyTheme.button
: Theme.of(context).cardColor, : Theme.of(context).cardColor,
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(5.0), Radius.circular(5.0),
), ),
border: rightClickEntry.value == entry
? Border.all(
color: MyTheme.button,
width: 1.0,
)
: null,
), ),
key: ValueKey(entry.name), key: ValueKey(entry.name),
height: kDesktopFileTransferRowHeight, height: kDesktopFileTransferRowHeight,
@@ -1050,51 +1105,19 @@ class _FileManagerViewState extends State<FileManagerView> {
), ),
Expanded( Expanded(
child: Text(entry.name.nonBreaking, child: Text(entry.name.nonBreaking,
style: TextStyle(
color: selectedItems.items
.contains(entry)
? Colors.white
: null),
overflow: overflow:
TextOverflow.ellipsis)) TextOverflow.ellipsis))
]), ]),
)), )),
), ),
onTap: () { onTap: onTap,
final items = selectedItems; onSecondaryTap: onSecondaryTap,
// handle double click onSecondaryTapDown: onSecondaryTapDown,
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);
},
), ),
SizedBox( SizedBox(
width: 2.0, width: 2.0,
@@ -1111,11 +1134,17 @@ class _FileManagerViewState extends State<FileManagerView> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: MyTheme.darkGray, color: selectedItems.items
.contains(entry)
? Colors.white70
: MyTheme.darkGray,
), ),
)), )),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
// Divider from header. // Divider from header.
SizedBox( SizedBox(
@@ -1131,9 +1160,16 @@ class _FileManagerViewState extends State<FileManagerView> {
sizeStr, sizeStr,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 10, color: MyTheme.darkGray), fontSize: 10,
color:
selectedItems.items.contains(entry)
? Colors.white70
: MyTheme.darkGray),
), ),
), ),
onTap: onTap,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
), ),
), ),
], ],

View File

@@ -245,8 +245,10 @@ class _RemotePageState extends State<RemotePage>
super.dispose(); super.dispose();
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
_ffi.textureModel.onRemotePageDispose(closeSession); _ffi.textureModel.onRemotePageDispose(closeSession);
// ensure we leave this session, this is a double check if (closeSession) {
_ffi.inputModel.enterOrLeave(false); // ensure we leave this session, this is a double check
_ffi.inputModel.enterOrLeave(false);
}
DesktopMultiWindow.removeListener(this); DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay(); _ffi.dialogManager.hideMobileActionsOverlay();
_ffi.imageModel.disposeImage(); _ffi.imageModel.disposeImage();

View File

@@ -178,8 +178,9 @@ String getLocalPlatformForKBLayoutType(String peerPlatform) {
localPlatform = kPeerPlatformWindows; localPlatform = kPeerPlatformWindows;
} else if (isLinux) { } else if (isLinux) {
localPlatform = kPeerPlatformLinux; localPlatform = kPeerPlatformLinux;
} else if (isWebOnWindows || isWebOnLinux) {
localPlatform = kPeerPlatformWebDesktop;
} }
// to-do: web desktop support ?
return localPlatform; return localPlatform;
} }

View File

@@ -452,8 +452,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
Widget _buildToolbar(BuildContext context) { Widget _buildToolbar(BuildContext context) {
final List<Widget> toolbarItems = []; final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) { if (!isWebDesktop) {
toolbarItems.add(_PinMenu(state: widget.state));
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
} }
@@ -1612,7 +1612,9 @@ class _KeyboardMenu extends StatelessWidget {
// If use flutter to grab keys, we can only use one mode. // If use flutter to grab keys, we can only use one mode.
// Map mode and Legacy mode, at least one of them is supported. // Map mode and Legacy mode, at least one of them is supported.
String? modeOnly; 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( if (bind.sessionIsKeyboardModeSupported(
sessionId: ffi.sessionId, mode: kKeyMapMode)) { sessionId: ffi.sessionId, mode: kKeyMapMode)) {
modeOnly = kKeyMapMode; modeOnly = kKeyMapMode;
@@ -1716,7 +1718,9 @@ class _KeyboardMenu extends StatelessWidget {
if (value == null) return; if (value == null) return;
await bind.sessionToggleOption( await bind.sessionToggleOption(
sessionId: ffi.sessionId, value: kOptionToggleViewOnly); 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, : null,
ffi: ffi, ffi: ffi,

View File

@@ -552,6 +552,13 @@ class _DesktopTabState extends State<DesktopTab>
controller: state.value.pageController, controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
children: () { 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. /// 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 /// [workaround] PageView children need an immutable list, after it has been passed into PageView
final tabLen = state.value.tabs.length; final tabLen = state.value.tabs.length;

View File

@@ -372,7 +372,7 @@ class App extends StatefulWidget {
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
class _AppState extends State<App> { class _AppState extends State<App> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -396,6 +396,34 @@ class _AppState extends State<App> {
bind.mainChangeTheme(dark: to.toShortString()); 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 @override

View File

@@ -9,19 +9,16 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/autocomplete.dart'; import '../../common/widgets/autocomplete.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import 'home_page.dart'; import 'home_page.dart';
import 'scan_page.dart';
import 'settings_page.dart';
/// Connection page for connecting to a remote peer. /// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget implements PageShape { class ConnectionPage extends StatefulWidget implements PageShape {
ConnectionPage({Key? key}) : super(key: key); ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
@override @override
final icon = const Icon(Icons.connected_tv); final icon = const Icon(Icons.connected_tv);
@@ -30,7 +27,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
final title = translate("Connection"); final title = translate("Connection");
@override @override
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[]; final List<Widget> appBarActions;
@override @override
State<ConnectionPage> createState() => _ConnectionPageState(); State<ConnectionPage> createState() => _ConnectionPageState();
@@ -73,9 +70,17 @@ class _ConnectionPageState extends State<ConnectionPage> {
} }
if (isAndroid) { if (isAndroid) {
if (!bind.isCustomClient()) { if (!bind.isCustomClient()) {
platformFFI.registerEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
(Map<String, dynamic> evt) async {
if (evt['url'] is String) {
setState(() {
_updateUrl = evt['url'];
});
}
});
Timer(const Duration(seconds: 1), () async { Timer(const Duration(seconds: 1), () async {
_updateUrl = await bind.mainGetSoftwareUpdateUrl(); bind.mainGetSoftwareUpdateUrl();
if (_updateUrl.isNotEmpty) setState(() {});
}); });
} }
} }
@@ -252,6 +257,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
), ),
), ),
inputFormatters: [IDTextInputFormatter()], inputFormatters: [IDTextInputFormatter()],
onSubmitted: (_) {
onConnect();
},
); );
}, },
onSelected: (option) { onSelected: (option) {
@@ -353,76 +361,10 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (Get.isRegistered<IDTextEditingController>()) { if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>(); Get.delete<IDTextEditingController>();
} }
if (!bind.isCustomClient()) {
platformFFI.unregisterEventHandler(
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
}
super.dispose(); super.dispose();
} }
} }
class WebMenu extends StatefulWidget {
const WebMenu({Key? key}) : super(key: key);
@override
State<WebMenu> createState() => _WebMenuState();
}
class _WebMenuState extends State<WebMenu> {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
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<String>>[]) +
[
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(),
),
);
}
});
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_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 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/chat_page.dart'; import '../../common/widgets/chat_page.dart';
@@ -45,7 +46,11 @@ class HomePageState extends State<HomePage> {
void initPages() { void initPages() {
_pages.clear(); _pages.clear();
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); if (!bind.isIncomingOnly()) {
_pages.add(ConnectionPage(
appBarActions: [],
));
}
if (isAndroid && !bind.isOutgoingOnly()) { if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length; _chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
@@ -149,7 +154,8 @@ class HomePageState extends State<HomePage> {
} }
class WebHomePage extends StatelessWidget { class WebHomePage extends StatelessWidget {
final connectionPage = ConnectionPage(); final connectionPage =
ConnectionPage(appBarActions: <Widget>[const WebSettingsPage()]);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -19,95 +19,48 @@ class ScanPage extends StatefulWidget {
class _ScanPageState extends State<ScanPage> { class _ScanPageState extends State<ScanPage> {
QRViewController? controller; QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); 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 @override
void reassemble() { void reassemble() {
super.reassemble(); super.reassemble();
if (isAndroid) { if (isAndroid && controller != null) {
controller!.pauseCamera(); controller!.pauseCamera();
} else if (controller != null) {
controller!.resumeCamera();
} }
controller!.resumeCamera();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Scan QR'), title: const Text('Scan QR'),
actions: [ actions: [
IconButton( _buildImagePickerButton(),
color: Colors.white, _buildFlashToggleButton(),
icon: Icon(Icons.image_search), _buildCameraSwitchButton(),
iconSize: 32.0, ],
onPressed: () async { ),
final ImagePicker picker = ImagePicker(); body: _buildQrView(context),
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));
} }
Widget _buildQrView(BuildContext 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 ||
var scanArea = (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400
MediaQuery.of(context).size.height < 400)
? 150.0 ? 150.0
: 300.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( return QRView(
key: qrKey, key: qrKey,
onQRViewCreated: _onQRViewCreated, onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape( overlay: QrScannerOverlayShape(
borderColor: Colors.red, borderColor: Colors.red,
borderRadius: 10, borderRadius: 10,
borderLength: 30, borderLength: 30,
borderWidth: 10, borderWidth: 10,
cutOutSize: scanArea), cutOutSize: scanArea,
),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p), onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
); );
} }
@@ -116,7 +69,7 @@ class _ScanPageState extends State<ScanPage> {
setState(() { setState(() {
this.controller = controller; this.controller = controller;
}); });
controller.scannedDataStream.listen((scanData) { scanSubscription = controller.scannedDataStream.listen((scanData) {
if (scanData.code != null) { if (scanData.code != null) {
showServerSettingFromQr(scanData.code!); showServerSettingFromQr(scanData.code!);
} }
@@ -129,8 +82,66 @@ class _ScanPageState extends State<ScanPage> {
} }
} }
Future<void> _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 @override
void dispose() { void dispose() {
scanSubscription?.cancel();
controller?.dispose(); controller?.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -66,10 +66,16 @@ class AbModel {
var listInitialized = false; var listInitialized = false;
var _maxPeerOneAb = 0; var _maxPeerOneAb = 0;
late final Peers peersModel;
WeakReference<FFI> parent; WeakReference<FFI> parent;
AbModel(this.parent) { AbModel(this.parent) {
addressbooks.clear(); addressbooks.clear();
peersModel = Peers(
name: PeersModelName.addressBook,
getInitPeers: () => currentAbPeers,
loadEvent: LoadEvent.addressBook);
if (desktopType == DesktopType.main) { if (desktopType == DesktopType.main) {
Timer.periodic(Duration(milliseconds: 500), (timer) async { Timer.periodic(Duration(milliseconds: 500), (timer) async {
if (_timerCounter++ % 6 == 0) { if (_timerCounter++ % 6 == 0) {

View File

@@ -181,6 +181,7 @@ class TextureModel {
} }
updateCurrentDisplay(int curDisplay) { updateCurrentDisplay(int curDisplay) {
if (isWeb) return;
final ffi = parent.target; final ffi = parent.target;
if (ffi == null) return; if (ffi == null) return;
tryCreateTexture(int idx) { tryCreateTexture(int idx) {

View File

@@ -34,6 +34,7 @@ class JobID {
} }
typedef GetSessionID = SessionID Function(); typedef GetSessionID = SessionID Function();
typedef GetDialogManager = OverlayDialogManager? Function();
class FileModel { class FileModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
@@ -45,13 +46,15 @@ class FileModel {
late final FileController remoteController; late final FileController remoteController;
late final GetSessionID getSessionID; late final GetSessionID getSessionID;
late final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
late final FileDialogEventLoop evtLoop; late final FileDialogEventLoop evtLoop;
FileModel(this.parent) { FileModel(this.parent) {
getSessionID = () => parent.target!.sessionId; getSessionID = () => parent.target!.sessionId;
getDialogManager = () => parent.target?.dialogManager;
fileFetcher = FileFetcher(getSessionID); fileFetcher = FileFetcher(getSessionID);
jobController = JobController(getSessionID); jobController = JobController(getSessionID, getDialogManager);
localController = FileController( localController = FileController(
isLocal: true, isLocal: true,
getSessionID: getSessionID, getSessionID: getSessionID,
@@ -451,7 +454,7 @@ class FileController {
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
for (var from in items.items) { for (var from in items.items) {
final jobID = jobController.add(from, isRemoteToLocal); final jobID = jobController.addTransferJob(from, isRemoteToLocal);
bind.sessionSendFiles( bind.sessionSendFiles(
sessionId: sessionId, sessionId: sessionId,
actId: jobID, actId: jobID,
@@ -494,13 +497,21 @@ class FileController {
fd.format(isWindows); fd.format(isWindows);
dialogManager?.dismissAll(); dialogManager?.dismissAll();
if (fd.entries.isEmpty) { if (fd.entries.isEmpty) {
var deleteJobId = jobController.addDeleteDirJob(item, !isLocal, 0);
final confirm = await showRemoveDialog( final confirm = await showRemoveDialog(
translate( translate(
"Are you sure you want to delete this empty directory?"), "Are you sure you want to delete this empty directory?"),
item.name, item.name,
false); false);
if (confirm == true) { if (confirm == true) {
sendRemoveEmptyDir(item.path, 0); sendRemoveEmptyDir(
item.path,
0,
deleteJobId,
);
} else {
jobController.updateJobStatus(deleteJobId,
error: "cancel", state: JobState.done);
} }
return; return;
} }
@@ -508,6 +519,13 @@ class FileController {
} else { } else {
entries = []; 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++) { for (var i = 0; i < entries.length; i++) {
final dirShow = item.isDirectory final dirShow = item.isDirectory
@@ -522,24 +540,32 @@ class FileController {
); );
try { try {
if (confirm == true) { if (confirm == true) {
sendRemoveFile(entries[i].path, i); sendRemoveFile(entries[i].path, i, deleteJobId);
final res = await jobController.jobResultListener.start(); final res = await jobController.jobResultListener.start();
// handle remove res; // handle remove res;
if (item.isDirectory && if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) { 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 (_removeCheckboxRemember) {
if (confirm == true) { if (confirm == true) {
for (var j = i + 1; j < entries.length; j++) { 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(); final res = await jobController.jobResultListener.start();
if (item.isDirectory && if (item.isDirectory &&
res['file_num'] == (entries.length - 1).toString()) { 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; break;
} }
@@ -618,22 +644,19 @@ class FileController {
}, useAnimation: false); }, useAnimation: false);
} }
void sendRemoveFile(String path, int fileNum) { void sendRemoveFile(String path, int fileNum, int actId) {
bind.sessionRemoveFile( bind.sessionRemoveFile(
sessionId: sessionId, sessionId: sessionId,
actId: JobController.jobID.next(), actId: actId,
path: path, path: path,
isRemote: !isLocal, isRemote: !isLocal,
fileNum: fileNum); fileNum: fileNum);
} }
void sendRemoveEmptyDir(String path, int fileNum) { void sendRemoveEmptyDir(String path, int fileNum, int actId) {
history.removeWhere((element) => element.contains(path)); history.removeWhere((element) => element.contains(path));
bind.sessionRemoveAllEmptyDirs( bind.sessionRemoveAllEmptyDirs(
sessionId: sessionId, sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
actId: JobController.jobID.next(),
path: path,
isRemote: !isLocal);
} }
Future<void> createDir(String path) async { Future<void> createDir(String path) async {
@@ -716,27 +739,29 @@ class FileController {
} }
} }
const _kOneWayFileTransferError = 'one-way-file-transfer-tip';
class JobController { class JobController {
static final JobID jobID = JobID(); static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs; final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>(); final jobResultListener = JobResultListener<Map<String, dynamic>>();
final GetSessionID getSessionID; final GetSessionID getSessionID;
final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); 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) { int getJob(int id) {
return jobTable.indexWhere((element) => element.id == id); return jobTable.indexWhere((element) => element.id == id);
} }
// JobProgress? getJob(int id) {
// return jobTable.firstWhere((element) => element.id == id);
// }
// return jobID // return jobID
int add(Entry from, bool isRemoteToLocal) { int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next(); final jobID = JobController.jobID.next();
jobTable.add(JobProgress() jobTable.add(JobProgress()
..type = JobType.transfer
..fileName = path.basename(from.path) ..fileName = path.basename(from.path)
..jobName = from.path ..jobName = from.path
..totalSize = from.size ..totalSize = from.size
@@ -746,6 +771,33 @@ class JobController {
return jobID; 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<String, dynamic> evt) { void tryUpdateJobProgress(Map<String, dynamic> evt) {
try { try {
int id = int.parse(evt['id']); int id = int.parse(evt['id']);
@@ -756,6 +808,7 @@ class JobController {
job.fileNum = int.parse(evt['file_num']); job.fileNum = int.parse(evt['file_num']);
job.speed = double.parse(evt['speed']); job.speed = double.parse(evt['speed']);
job.finishedSize = int.parse(evt['finished_size']); job.finishedSize = int.parse(evt['finished_size']);
job.recvJobRes = true;
debugPrint("update job $id with $evt"); debugPrint("update job $id with $evt");
jobTable.refresh(); jobTable.refresh();
} }
@@ -764,20 +817,48 @@ class JobController {
} }
} }
void jobDone(Map<String, dynamic> evt) async { Future<bool> jobDone(Map<String, dynamic> evt) async {
if (jobResultListener.isListening) { if (jobResultListener.isListening) {
jobResultListener.complete(evt); jobResultListener.complete(evt);
return; // return;
} }
int id = -1;
int id = int.parse(evt['id']); int? fileNum = 0;
double? speed = 0;
try {
id = int.parse(evt['id']);
} catch (_) {}
final jobIndex = getJob(id); final jobIndex = getJob(id);
if (jobIndex != -1) { if (jobIndex == -1) return true;
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.finishedSize = job.totalSize; job.recvJobRes = true;
if (job.type == JobType.deleteFile) {
job.state = JobState.done; job.state = JobState.done;
job.fileNum = int.parse(evt['file_num']); } else if (job.type == JobType.deleteDir) {
jobTable.refresh(); 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;
}
jobTable.refresh();
if (job.type == JobType.deleteDir) {
return job.state == JobState.done;
} else {
return true;
} }
} }
@@ -788,16 +869,61 @@ class JobController {
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.state = JobState.error; job.state = JobState.error;
job.err = err; job.err = err;
job.fileNum = int.parse(evt['file_num']); job.recvJobRes = true;
if (err == "skipped") { if (job.type == JobType.transfer) {
job.state = JobState.done; int? fileNum = int.tryParse(evt['file_num']);
job.finishedSize = job.totalSize; 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(); 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"); 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<void> cancelJob(int id) async { Future<void> cancelJob(int id) async {
await bind.sessionCancelJob(sessionId: sessionId, actId: id); await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} }
@@ -814,6 +940,7 @@ class JobController {
final currJobId = JobController.jobID.next(); final currJobId = JobController.jobID.next();
String fileName = path.basename(isRemote ? remote : to); String fileName = path.basename(isRemote ? remote : to);
var jobProgress = JobProgress() var jobProgress = JobProgress()
..type = JobType.transfer
..fileName = fileName ..fileName = fileName
..jobName = isRemote ? remote : to ..jobName = isRemote ? remote : to
..id = currJobId ..id = currJobId
@@ -1088,8 +1215,12 @@ extension JobStateDisplay on JobState {
} }
} }
enum JobType { none, transfer, deleteFile, deleteDir }
class JobProgress { class JobProgress {
JobType type = JobType.none;
JobState state = JobState.none; JobState state = JobState.none;
var recvJobRes = false;
var id = 0; var id = 0;
var fileNum = 0; var fileNum = 0;
var speed = 0.0; var speed = 0.0;
@@ -1109,7 +1240,9 @@ class JobProgress {
int lastTransferredSize = 0; int lastTransferredSize = 0;
clear() { clear() {
type = JobType.none;
state = JobState.none; state = JobState.none;
recvJobRes = false;
id = 0; id = 0;
fileNum = 0; fileNum = 0;
speed = 0; speed = 0;
@@ -1123,11 +1256,81 @@ class JobProgress {
} }
String display() { String display() {
if (state == JobState.done && err == "skipped") { if (type == JobType.transfer) {
return translate("Skipped"); if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
} else if (type == JobType.deleteFile) {
if (err == "cancel") {
return translate("Cancel");
}
} }
return state.display(); 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 { class _PathStat {

View File

@@ -23,7 +23,14 @@ class GroupModel {
bool get emtpy => users.isEmpty && peers.isEmpty; 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<void> pull({force = true, quiet = false}) async { Future<void> pull({force = true, quiet = false}) async {
if (bind.isDisableGroupPanel()) return; if (bind.isDisableGroupPanel()) return;

View File

@@ -177,7 +177,7 @@ class PointerEventToRust {
} }
} }
class ToReleaseKeys { class ToReleaseRawKeys {
RawKeyEvent? lastLShiftKeyEvent; RawKeyEvent? lastLShiftKeyEvent;
RawKeyEvent? lastRShiftKeyEvent; RawKeyEvent? lastRShiftKeyEvent;
RawKeyEvent? lastLCtrlKeyEvent; 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 { class InputModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
String keyboardMode = ''; String keyboardMode = '';
@@ -292,6 +334,7 @@ class InputModel {
var alt = false; var alt = false;
var command = false; var command = false;
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
final ToReleaseKeys toReleaseKeys = ToReleaseKeys(); final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
// trackpad // 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) { KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled; if (isViewOnly) return KeyEventResult.handled;
if ((isDesktop || isWebDesktop) && !isInputSourceFlutter) { if (!isInputSourceFlutter) {
return KeyEventResult.handled; if (isDesktop) {
return KeyEventResult.handled;
} else if (isWeb) {
return KeyEventResult.ignored;
}
} }
final key = e.logicalKey; final key = e.logicalKey;
@@ -358,7 +490,7 @@ class InputModel {
command = true; command = true;
} }
} }
toReleaseKeys.updateKeyDown(key, e); toReleaseRawKeys.updateKeyDown(key, e);
} }
if (e is RawKeyUpEvent) { if (e is RawKeyUpEvent) {
if (key == LogicalKeyboardKey.altLeft || if (key == LogicalKeyboardKey.altLeft ||
@@ -376,12 +508,50 @@ class InputModel {
command = false; command = false;
} }
toReleaseKeys.updateKeyUp(key, e); toReleaseRawKeys.updateKeyUp(key, e);
} }
// * Currently mobile does not enable map mode // * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == 'map') { if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardMode(e); 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);
}
// * Currently mobile does not enable map mode
if ((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 { } else {
legacyKeyboardMode(e); legacyKeyboardMode(e);
} }
@@ -389,7 +559,33 @@ class InputModel {
return KeyEventResult.handled; 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 positionCode = -1;
int platformCode = -1; int platformCode = -1;
bool down; bool down;
@@ -441,7 +637,7 @@ class InputModel {
.contains(KeyboardLockMode.scrollLock)) { .contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock); lockModes |= (1 << scrolllock);
} }
bind.sessionHandleFlutterKeyEvent( bind.sessionHandleFlutterRawKeyEvent(
sessionId: sessionId, sessionId: sessionId,
name: name, name: name,
platformCode: platformCode, platformCode: platformCode,
@@ -450,7 +646,7 @@ class InputModel {
downOrUp: down); downOrUp: down);
} }
void legacyKeyboardMode(RawKeyEvent e) { void legacyKeyboardModeRaw(RawKeyEvent e) {
if (e is RawKeyDownEvent) { if (e is RawKeyDownEvent) {
if (e.repeat) { if (e.repeat) {
sendRawKey(e, press: true); sendRawKey(e, press: true);
@@ -471,6 +667,24 @@ class InputModel {
inputKey(label, down: down, press: press ?? false); 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. /// Send key stroke event.
/// [down] indicates the key's state(down or up). /// [down] indicates the key's state(down or up).
/// [press] indicates a click event(down and up). /// [press] indicates a click event(down and up).
@@ -566,7 +780,8 @@ class InputModel {
} }
void enterOrLeave(bool enter) { void enterOrLeave(bool enter) {
toReleaseKeys.release(handleRawKeyEvent); toReleaseKeys.release(handleKeyEvent);
toReleaseRawKeys.release(handleRawKeyEvent);
_pointerMovedAfterEnter = false; _pointerMovedAfterEnter = false;
// Fix status // Fix status
@@ -577,6 +792,9 @@ class InputModel {
if (!isInputSourceFlutter) { if (!isInputSourceFlutter) {
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
} }
if (!isWeb && enter) {
bind.setCurSessionId(sessionId: sessionId);
}
} }
/// Send mouse movement event with distance in [x] and [y]. /// Send mouse movement event with distance in [x] and [y].
@@ -1164,15 +1382,15 @@ class InputModel {
// Simulate a key press event. // Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key. // `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async { Future<void> tapHidKey(int usbHidUsage) async {
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, true); newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
await Future.delayed(Duration(milliseconds: 100)); await Future.delayed(Duration(milliseconds: 100));
inputRawKey(kKeyFlutterKey, usbHidUsage, 0, false); newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
} }
Future<void> onMobileVolumeUp() async => Future<void> onMobileVolumeUp() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
Future<void> onMobileVolumeDown() async => Future<void> onMobileVolumeDown() async =>
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
Future<void> onMobilePower() async => Future<void> onMobilePower() async =>
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage); await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
} }

View File

@@ -7,12 +7,14 @@ import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart'; import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_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/peer_tab_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/user_model.dart';
@@ -304,8 +306,13 @@ class FfiModel with ChangeNotifier {
} else if (name == 'job_progress') { } else if (name == 'job_progress') {
parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
} else if (name == 'job_done') { } else if (name == 'job_done') {
parent.target?.fileModel.jobController.jobDone(evt); bool? refresh =
parent.target?.fileModel.refreshAll(); 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') { } else if (name == 'job_error') {
parent.target?.fileModel.jobController.jobError(evt); parent.target?.fileModel.jobController.jobError(evt);
} else if (name == 'override_file_confirm') { } else if (name == 'override_file_confirm') {
@@ -492,10 +499,12 @@ class FfiModel with ChangeNotifier {
newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width;
newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height;
newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1;
newDisplay.originalWidth = newDisplay.originalWidth = int.tryParse(
int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; evt['original_width'] ?? kInvalidResolutionValue.toString()) ??
newDisplay.originalHeight = kInvalidResolutionValue;
int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; newDisplay.originalHeight = int.tryParse(
evt['original_height'] ?? kInvalidResolutionValue.toString()) ??
kInvalidResolutionValue;
newDisplay._scale = _pi.scaleOfDisplay(display); newDisplay._scale = _pi.scaleOfDisplay(display);
_pi.displays[display] = newDisplay; _pi.displays[display] = newDisplay;
@@ -788,7 +797,7 @@ class FfiModel with ChangeNotifier {
isRefreshing = false; isRefreshing = false;
} }
Map<String, dynamic> features = json.decode(evt['features']); Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1; _pi.features.privacyMode = features['privacy_mode'] == true;
if (!isCache) { if (!isCache) {
handleResolutions(peerId, evt["resolutions"]); handleResolutions(peerId, evt["resolutions"]);
} }
@@ -832,7 +841,7 @@ class FfiModel with ChangeNotifier {
for (final mode in [kKeyMapMode, kKeyLegacyMode]) { for (final mode in [kKeyMapMode, kKeyLegacyMode]) {
if (bind.sessionIsKeyboardModeSupported( if (bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: mode)) { sessionId: sessionId, mode: mode)) {
bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); await bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode);
break; break;
} }
} }
@@ -2178,6 +2187,7 @@ class CursorModel with ChangeNotifier {
debugPrint("deleting cursor with key $k"); debugPrint("deleting cursor with key $k");
deleteCustomCursor(k); deleteCustomCursor(k);
} }
resetSystemCursor();
} }
trySetRemoteWindowCoords() { trySetRemoteWindowCoords() {
@@ -2224,8 +2234,10 @@ class QualityMonitorModel with ChangeNotifier {
updateQualityStatus(Map<String, dynamic> evt) { updateQualityStatus(Map<String, dynamic> evt) {
try { try {
if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; if (evt.containsKey('speed') && (evt['speed'] as String).isNotEmpty) {
if ((evt['fps'] as String).isNotEmpty) { _data.speed = evt['speed'];
}
if (evt.containsKey('fps') && (evt['fps'] as String).isNotEmpty) {
final fps = jsonDecode(evt['fps']) as Map<String, dynamic>; final fps = jsonDecode(evt['fps']) as Map<String, dynamic>;
final pi = parent.target?.ffiModel.pi; final pi = parent.target?.ffiModel.pi;
if (pi != null) { if (pi != null) {
@@ -2246,14 +2258,18 @@ class QualityMonitorModel with ChangeNotifier {
_data.fps = null; _data.fps = null;
} }
} }
if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; if (evt.containsKey('delay') && (evt['delay'] as String).isNotEmpty) {
if ((evt['target_bitrate'] as String).isNotEmpty) { _data.delay = evt['delay'];
}
if (evt.containsKey('target_bitrate') &&
(evt['target_bitrate'] as String).isNotEmpty) {
_data.targetBitrate = evt['target_bitrate']; _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']; _data.codecFormat = evt['codec_format'];
} }
if ((evt['chroma'] as String).isNotEmpty) { if (evt.containsKey('chroma') && (evt['chroma'] as String).isNotEmpty) {
_data.chroma = evt['chroma']; _data.chroma = evt['chroma'];
} }
notifyListeners(); notifyListeners();
@@ -2383,6 +2399,9 @@ class FFI {
late final ElevationModel elevationModel; // session late final ElevationModel elevationModel; // session
late final CmFileModel cmFileModel; // cm late final CmFileModel cmFileModel; // cm
late final TextureModel textureModel; //session late final TextureModel textureModel; //session
late final Peers recentPeersModel; // global
late final Peers favoritePeersModel; // global
late final Peers lanPeersModel; // global
FFI(SessionID? sId) { FFI(SessionID? sId) {
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
@@ -2403,6 +2422,16 @@ class FFI {
elevationModel = ElevationModel(WeakReference(this)); elevationModel = ElevationModel(WeakReference(this));
cmFileModel = CmFileModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this));
textureModel = TextureModel(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 /// Mobile reuse FFI
@@ -2497,6 +2526,7 @@ class FFI {
onEvent2UIRgba(); onEvent2UIRgba();
imageModel.onRgba(display, data); imageModel.onRgba(display, data);
}); });
this.id = id;
return; return;
} }

View File

@@ -194,10 +194,14 @@ class Peers extends ChangeNotifier {
} }
void _updateOnlineState(Map<String, dynamic> evt) { void _updateOnlineState(Map<String, dynamic> evt) {
int changedCount = 0;
evt['onlines'].split(',').forEach((online) { evt['onlines'].split(',').forEach((online) {
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (peers[i].id == online) { 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) { evt['offlines'].split(',').forEach((offline) {
for (var i = 0; i < peers.length; i++) { for (var i = 0; i < peers.length; i++) {
if (peers[i].id == offline) { if (peers[i].id == offline) {
peers[i].online = false; if (peers[i].online) {
changedCount += 1;
peers[i].online = false;
}
} }
} }
}); });
event = UpdateEvent.online; if (changedCount > 0) {
notifyListeners(); event = UpdateEvent.online;
notifyListeners();
}
} }
void _updatePeers(Map<String, dynamic> evt) { void _updatePeers(Map<String, dynamic> evt) {

View File

@@ -184,10 +184,17 @@ class PeerTabModel with ChangeNotifier {
notifyListeners(); 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<Peer> peers) { setCurrentTabCachedPeers(List<Peer> peers) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
final isPreEmpty = _currentTabCachedPeers.isEmpty;
_currentTabCachedPeers = peers; _currentTabCachedPeers = peers;
notifyListeners(); final isNowEmpty = _currentTabCachedPeers.isEmpty;
if (isPreEmpty != isNowEmpty) {
notifyListeners();
}
}); });
} }

View File

@@ -826,7 +826,7 @@ class Client {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['is_start'] = authorized; data['authorized'] = authorized;
data['is_file_transfer'] = isFileTransfer; data['is_file_transfer'] = isFileTransfer;
data['port_forward'] = portForward; data['port_forward'] = portForward;
data['name'] = name; data['name'] = name;
@@ -840,6 +840,8 @@ class Client {
data['block_input'] = blockInput; data['block_input'] = blockInput;
data['disconnected'] = disconnected; data['disconnected'] = disconnected;
data['from_switch'] = fromSwitch; data['from_switch'] = fromSwitch;
data['in_voice_call'] = inVoiceCall;
data['incoming_voice_call'] = incomingVoiceCall;
return data; return data;
} }

View File

@@ -20,6 +20,8 @@ class StateGlobal {
final svcStatus = SvcStatus.notReady.obs; final svcStatus = SvcStatus.notReady.obs;
final RxBool isFocused = false.obs; final RxBool isFocused = false.obs;
final isPortrait = false.obs;
String _inputSource = ''; String _inputSource = '';
// Use for desktop -> remote toolbar -> resolution // Use for desktop -> remote toolbar -> resolution

View File

@@ -11,3 +11,7 @@ final isWebDesktop_ = false;
final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux; final isDesktop_ = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
String get screenInfo_ => ''; String get screenInfo_ => '';
final isWebOnWindows_ = false;
final isWebOnLinux_ = false;
final isWebOnMacOS_ = false;

View File

@@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/model.dart';
deleteCustomCursor(String key) => deleteCustomCursor(String key) =>
custom_cursor_manager.CursorManager.instance.deleteCursor(key); custom_cursor_manager.CursorManager.instance.deleteCursor(key);
resetSystemCursor() {}
MouseCursor buildCursorOfCache( MouseCursor buildCursorOfCache(
CursorModel cursor, double scale, CursorData? cache) { CursorModel cursor, double scale, CursorData? cache) {

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'dart:html' as html;
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@@ -23,6 +24,7 @@ sealed class EventToUI {
) = EventToUI_Rgba; ) = EventToUI_Rgba;
const factory EventToUI.texture( const factory EventToUI.texture(
int field0, int field0,
bool field1,
) = EventToUI_Texture; ) = EventToUI_Texture;
} }
@@ -33,15 +35,19 @@ class EventToUI_Event implements EventToUI {
} }
class EventToUI_Rgba 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; final int field;
int get field0 => field; int get field0 => field;
} }
class EventToUI_Texture implements EventToUI { class EventToUI_Texture implements EventToUI {
const EventToUI_Texture(final int field0) : this.field = field0; const EventToUI_Texture(final int field0, final bool field1)
final int field; : f0 = field0,
int get field0 => field; f1 = field1;
final int f0;
final bool f1;
int get field0 => f0;
bool get field1 => f1;
} }
class RustdeskImpl { class RustdeskImpl {
@@ -181,7 +187,7 @@ class RustdeskImpl {
Future<void> sessionToggleOption( Future<void> sessionToggleOption(
{required UuidValue sessionId, required String value, dynamic hint}) { {required UuidValue sessionId, required String value, dynamic hint}) {
return Future( return Future(
() => js.context.callMethod('setByName', ['toggle_option', value])); () => js.context.callMethod('setByName', ['option:toggle', value]));
} }
Future<void> sessionTogglePrivacyMode( Future<void> sessionTogglePrivacyMode(
@@ -190,8 +196,8 @@ class RustdeskImpl {
required bool on, required bool on,
dynamic hint}) { dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [ return Future(() => js.context.callMethod('setByName', [
'toggle_option', 'toggle_privacy_mode',
jsonEncode({implKey, on}) jsonEncode({'impl_key': implKey, 'on': on})
])); ]));
} }
@@ -229,7 +235,7 @@ class RustdeskImpl {
} }
String getLocalKbLayoutType({dynamic hint}) { String getLocalKbLayoutType({dynamic hint}) {
throw js.context.callMethod('getByName', ['option:local', 'kb_layout']); return js.context.callMethod('getByName', ['option:local', 'kb_layout']);
} }
Future<void> setLocalKbLayoutType( Future<void> setLocalKbLayoutType(
@@ -346,7 +352,11 @@ class RustdeskImpl {
bool sessionIsKeyboardModeSupported( bool sessionIsKeyboardModeSupported(
{required UuidValue sessionId, required String mode, dynamic hint}) { {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}) { bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
@@ -385,14 +395,32 @@ class RustdeskImpl {
return Future(() => js.context.callMethod('setByName', [ return Future(() => js.context.callMethod('setByName', [
'switch_display', 'switch_display',
jsonEncode({ jsonEncode({
isDesktop: isDesktop, 'isDesktop': isDesktop,
sessionId: sessionId.toString(), 'sessionId': sessionId.toString(),
value: value 'value': value
}) })
])); ]));
} }
Future<void> sessionHandleFlutterKeyEvent( Future<void> 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<void> sessionHandleFlutterRawKeyEvent(
{required UuidValue sessionId, {required UuidValue sessionId,
required String name, required String name,
required int platformCode, required int platformCode,
@@ -400,13 +428,12 @@ class RustdeskImpl {
required int lockModes, required int lockModes,
required bool downOrUp, required bool downOrUp,
dynamic hint}) { dynamic hint}) {
// TODO: map mode
throw UnimplementedError(); throw UnimplementedError();
} }
void sessionEnterOrLeave( void sessionEnterOrLeave(
{required UuidValue sessionId, required bool enter, dynamic hint}) { {required UuidValue sessionId, required bool enter, dynamic hint}) {
throw UnimplementedError(); js.context.callMethod('setByName', ['enter_or_leave', enter]);
} }
Future<void> sessionInputKey( Future<void> sessionInputKey(
@@ -449,7 +476,7 @@ class RustdeskImpl {
required String name, required String name,
required String value, required String value,
dynamic hint}) { dynamic hint}) {
return Future(() => js.context.callMethod('SetByName', [ return Future(() => js.context.callMethod('setByName', [
'option:session', 'option:session',
jsonEncode({'name': name, 'value': value}) jsonEncode({'name': name, 'value': value})
])); ]));
@@ -581,7 +608,7 @@ class RustdeskImpl {
Future<void> sessionElevateDirect( Future<void> sessionElevateDirect(
{required UuidValue sessionId, dynamic hint}) { {required UuidValue sessionId, dynamic hint}) {
throw UnimplementedError(); return Future(() => js.context.callMethod('setByName', ['elevate_direct']));
} }
Future<void> sessionElevateWithLogon( Future<void> sessionElevateWithLogon(
@@ -591,7 +618,7 @@ class RustdeskImpl {
dynamic hint}) { dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [ return Future(() => js.context.callMethod('setByName', [
'elevate_with_logon', 'elevate_with_logon',
jsonEncode({username, password}) jsonEncode({'username': username, 'password': password})
])); ]));
} }
@@ -633,7 +660,15 @@ class RustdeskImpl {
} }
String mainGetLoginDeviceInfo({dynamic hint}) { 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<void> mainChangeId({required String newId, dynamic hint}) { Future<void> mainChangeId({required String newId, dynamic hint}) {
@@ -702,11 +737,11 @@ class RustdeskImpl {
} }
Future<String> mainGetAppName({dynamic hint}) { Future<String> mainGetAppName({dynamic hint}) {
throw UnimplementedError(); return Future.value(mainGetAppNameSync(hint: hint));
} }
String mainGetAppNameSync({dynamic hint}) { String mainGetAppNameSync({dynamic hint}) {
throw UnimplementedError(); return 'RustDesk';
} }
String mainUriPrefixSync({dynamic hint}) { String mainUriPrefixSync({dynamic hint}) {
@@ -714,7 +749,8 @@ class RustdeskImpl {
} }
Future<String> mainGetLicense({dynamic hint}) { Future<String> mainGetLicense({dynamic hint}) {
throw UnimplementedError(); // TODO: implement
return Future(() => '');
} }
Future<String> mainGetVersion({dynamic hint}) { Future<String> mainGetVersion({dynamic hint}) {
@@ -758,8 +794,9 @@ class RustdeskImpl {
} }
Future<bool> mainIsUsingPublicServer({dynamic hint}) { Future<bool> mainIsUsingPublicServer({dynamic hint}) {
return Future( return Future(() =>
() => js.context.callMethod('setByName', ["is_using_public_server"])); js.context.callMethod('getByName', ["is_using_public_server"]) ==
'true');
} }
Future<void> mainDiscover({dynamic hint}) { Future<void> mainDiscover({dynamic hint}) {
@@ -813,24 +850,29 @@ class RustdeskImpl {
} }
String mainGetInputSource({dynamic hint}) { String mainGetInputSource({dynamic hint}) {
// // rdev grab mode final inputSource =
// const CONFIG_INPUT_SOURCE_1 = "Input source 1"; js.context.callMethod('getByName', ['option:local', 'input-source']);
// // js grab mode
// export const CONFIG_INPUT_SOURCE_1 = "Input source 1";
// // flutter grab mode // // flutter grab mode
// const CONFIG_INPUT_SOURCE_2 = "Input source 2"; // export const CONFIG_INPUT_SOURCE_2 = "Input source 2";
return 'Input source 2'; return inputSource != '' ? inputSource : 'Input source 1';
} }
Future<void> mainSetInputSource( Future<void> mainSetInputSource(
{required UuidValue sessionId, required String value, dynamic hint}) { {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<String> mainGetMyId({dynamic hint}) { Future<String> mainGetMyId({dynamic hint}) {
throw UnimplementedError(); return Future(() => js.context.callMethod('getByName', ['my_id']));
} }
Future<String> mainGetUuid({dynamic hint}) { Future<String> mainGetUuid({dynamic hint}) {
throw UnimplementedError(); return Future(() => js.context.callMethod('getByName', ['uuid']));
} }
Future<String> mainGetPeerOption( Future<String> mainGetPeerOption(
@@ -952,10 +994,11 @@ class RustdeskImpl {
Future<void> mainSetUserDefaultOption( Future<void> mainSetUserDefaultOption(
{required String key, required String value, dynamic hint}) { {required String key, required String value, dynamic hint}) {
return js.context.callMethod('getByName', [ js.context.callMethod('setByName', [
'option:user:default', 'option:user:default',
jsonEncode({'name': key, 'value': value}) jsonEncode({'name': key, 'value': value})
]); ]);
return Future.value();
} }
String mainGetUserDefaultOption({required String key, dynamic hint}) { String mainGetUserDefaultOption({required String key, dynamic hint}) {
@@ -1020,7 +1063,7 @@ class RustdeskImpl {
() => js.context.callMethod('getByName', ['option', 'last_remote_id'])); () => js.context.callMethod('getByName', ['option', 'last_remote_id']));
} }
Future<String> mainGetSoftwareUpdateUrl({dynamic hint}) { Future<void> mainGetSoftwareUpdateUrl({dynamic hint}) {
throw UnimplementedError(); throw UnimplementedError();
} }
@@ -1029,7 +1072,7 @@ class RustdeskImpl {
} }
Future<String> mainGetLangs({dynamic hint}) { Future<String> mainGetLangs({dynamic hint}) {
throw UnimplementedError(); return Future(() => js.context.callMethod('getByName', ['langs']));
} }
Future<String> mainGetTemporaryPassword({dynamic hint}) { Future<String> mainGetTemporaryPassword({dynamic hint}) {
@@ -1041,7 +1084,7 @@ class RustdeskImpl {
} }
Future<String> mainGetFingerprint({dynamic hint}) { Future<String> mainGetFingerprint({dynamic hint}) {
throw UnimplementedError(); return Future.value('');
} }
Future<String> cmGetClientsState({dynamic hint}) { Future<String> cmGetClientsState({dynamic hint}) {
@@ -1083,7 +1126,7 @@ class RustdeskImpl {
} }
String mainSupportedHwdecodings({dynamic hint}) { String mainSupportedHwdecodings({dynamic hint}) {
throw UnimplementedError(); return '{}';
} }
Future<bool> mainIsRoot({dynamic hint}) { Future<bool> mainIsRoot({dynamic hint}) {
@@ -1170,8 +1213,10 @@ class RustdeskImpl {
required int index, required int index,
required bool on, required bool on,
dynamic hint}) { dynamic hint}) {
// TODO return Future(() => js.context.callMethod('setByName', [
throw UnimplementedError(); 'toggle_virtual_display',
jsonEncode({'index': index, 'on': on})
]));
} }
Future<void> mainSetHomeDir({required String home, dynamic hint}) { Future<void> mainSetHomeDir({required String home, dynamic hint}) {
@@ -1272,8 +1317,7 @@ class RustdeskImpl {
} }
Future<String> mainGetBuildDate({dynamic hint}) { Future<String> mainGetBuildDate({dynamic hint}) {
// TODO return Future(() => js.context.callMethod('getByName', ['build_date']));
throw UnimplementedError();
} }
String translate( String translate(
@@ -1575,6 +1619,7 @@ class RustdeskImpl {
String mainSupportedInputSource({dynamic hint}) { String mainSupportedInputSource({dynamic hint}) {
return jsonEncode([ return jsonEncode([
['Input source 1', 'input_source_1_tip'],
['Input source 2', 'input_source_2_tip'] ['Input source 2', 'input_source_2_tip']
]); ]);
} }
@@ -1610,7 +1655,7 @@ class RustdeskImpl {
} }
bool mainIsOptionFixed({required String key, dynamic hint}) { bool mainIsOptionFixed({required String key, dynamic hint}) {
throw UnimplementedError(); return false;
} }
bool mainGetUseTextureRender({dynamic hint}) { bool mainGetUseTextureRender({dynamic hint}) {
@@ -1650,5 +1695,40 @@ class RustdeskImpl {
throw UnimplementedError(); throw UnimplementedError();
} }
Future<String> getVoiceCallInputDevice({required bool isCm, dynamic hint}) {
throw UnimplementedError();
}
Future<void> setVoiceCallInputDevice(
{required bool isCm, required String device, dynamic hint}) {
throw UnimplementedError();
}
bool isPresetPasswordMobileOnly({dynamic hint}) {
throw UnimplementedError();
}
String mainGetBuildinOption({required String key, dynamic hint}) {
return '';
}
String installInstallOptions({dynamic hint}) {
throw UnimplementedError();
}
int mainMaxEncryptLen({dynamic hint}) {
throw UnimplementedError();
}
sessionRenameFile(
{required UuidValue sessionId,
required int actId,
required String path,
required String newName,
required bool isRemote,
dynamic hint}) {
throw UnimplementedError();
}
void dispose() {} void dispose() {}
} }

View File

@@ -1,4 +1,7 @@
import 'dart:js' as js; 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 isAndroid_ = false;
final isIOS_ = false; final isIOS_ = false;
@@ -11,3 +14,8 @@ final isWebDesktop_ = !js.context.callMethod('isMobile');
final isDesktop_ = false; final isDesktop_ = false;
String get screenInfo_ => js.context.callMethod('getByName', ['screen_info']); 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;

View File

@@ -58,6 +58,11 @@ class CursorManager {
]); ]);
} }
} }
Future<void> resetSystemCursor() async {
latestKey = '';
js.context.callMethod('setByName', ['cursor', 'auto']);
}
} }
class FlutterCustomMemoryImageCursor extends MouseCursor { class FlutterCustomMemoryImageCursor extends MouseCursor {
@@ -92,6 +97,7 @@ class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
} }
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key); deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
MouseCursor buildCursorOfCache( MouseCursor buildCursorOfCache(
model.CursorModel cursor, double scale, model.CursorData? cache) { model.CursorModel cursor, double scale, model.CursorData? cache) {

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/mobile/pages/scan_page.dart';
import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:provider/provider.dart';
import '../../common.dart';
import '../../common/widgets/login.dart';
import '../../models/model.dart';
class WebSettingsPage extends StatelessWidget {
const WebSettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isWebDesktop) {
return _buildDesktopButton(context);
} else {
return _buildMobileMenu(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),
),
);
},
);
}
Widget _buildMobileMenu(BuildContext context) {
Provider.of<FfiModel>(context);
return PopupMenuButton<String>(
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<String>>[]) +
[
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(),
),
);
}
});
}
}

View File

@@ -6,7 +6,7 @@ class TextureRgbaRenderer {
} }
Future<bool> closeTexture(int key) { Future<bool> closeTexture(int key) {
throw UnimplementedError(); return Future(() => true);
} }
Future<bool> onRgba( Future<bool> onRgba(

View File

@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195

View File

@@ -380,6 +380,22 @@ packages:
url: "https://github.com/rustdesk-org/dynamic_layouts.git" url: "https://github.com/rustdesk-org/dynamic_layouts.git"
source: git source: git
version: "0.0.1+1" 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: external_path:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -509,8 +525,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "38951317afe79d953ab25733667bd96e172a80d3" ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
resolved-ref: "38951317afe79d953ab25733667bd96e172a80d3" resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d"
url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer"
source: git source: git
version: "0.0.1" version: "0.0.1"
@@ -1613,5 +1629,5 @@ packages:
source: hosted source: hosted
version: "0.2.1" version: "0.2.1"
sdks: sdks:
dart: ">=3.2.0 <4.0.0" dart: ">=3.3.0 <4.0.0"
flutter: ">=3.16.0" flutter: ">=3.19.0"

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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 # 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: environment:
sdk: '^3.1.0' sdk: '^3.1.0'
@@ -93,7 +93,7 @@ dependencies:
flutter_gpu_texture_renderer: flutter_gpu_texture_renderer:
git: git:
url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer
ref: 38951317afe79d953ab25733667bd96e172a80d3 ref: 2ded7f146437a761ffe6981e2f742038f85ca68d
uuid: ^3.0.7 uuid: ^3.0.7
auto_size_text_field: ^2.2.1 auto_size_text_field: ^2.2.1
flex_color_picker: ^3.3.0 flex_color_picker: ^3.3.0
@@ -104,6 +104,7 @@ dependencies:
pull_down_button: ^0.9.3 pull_down_button: ^0.9.3
device_info_plus: ^9.1.0 device_info_plus: ^9.1.0
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
extended_text: 13.0.0
dev_dependencies: dev_dependencies:
icons_launcher: ^2.0.4 icons_launcher: ^2.0.4

View File

@@ -5,7 +5,7 @@ use std::{
}; };
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[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::{ use hbb_common::{
lazy_static, lazy_static,
tokio::sync::{ tokio::sync::{
@@ -25,6 +25,8 @@ pub use context_send::*;
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; 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; 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!( matches!(
self, self,
ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. }
@@ -198,7 +200,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline] #[inline]
fn send_data(conn_id: i32, data: ClipboardFile) { fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return send_data_to_channel(conn_id, data); return send_data_to_channel(conn_id, data);
#[cfg(not(target_os = "windows"))] #[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",))] #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline] #[inline]
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) { fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
// no need to handle result here
if let Some(msg_channel) = VEC_MSG_CHANNEL if let Some(msg_channel) = VEC_MSG_CHANNEL
.read() .read()
.unwrap() .unwrap()
.iter() .iter()
.find(|x| x.conn_id == conn_id) .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")] #[cfg(feature = "unix-file-copy-paste")]
#[inline] #[inline]
fn send_data_to_all(data: ClipboardFile) { fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
// no need to handle result here // Need more tests to see if it's necessary to handle the error.
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
allow_err!(msg_channel.sender.send(data.clone())); allow_err!(msg_channel.sender.send(data.clone()));
} }
Ok(())
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -7,7 +7,7 @@
use crate::{ use crate::{
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, 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 hbb_common::log;
use std::{ use std::{
@@ -998,7 +998,7 @@ extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE)
} }
}; };
// no need to handle result here // no need to handle result here
send_data(conn_id as _, data); allow_err!(send_data(conn_id as _, data));
0 0
} }
@@ -1045,7 +1045,13 @@ extern "C" fn client_format_list(
.iter() .iter()
.for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone())));
} else { } 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 0
@@ -1067,9 +1073,13 @@ extern "C" fn client_format_list_response(
msg_flags msg_flags
); );
let data = ClipboardFile::FormatListResponse { msg_flags }; let data = ClipboardFile::FormatListResponse { msg_flags };
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send format list response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_format_data_request( extern "C" fn client_format_data_request(
@@ -1090,10 +1100,13 @@ extern "C" fn client_format_data_request(
conn_id, conn_id,
requested_format_id requested_format_id
); );
// no need to handle result here match send_data(conn_id, data) {
send_data(conn_id, data); Ok(_) => 0,
Err(e) => {
0 log::error!("failed to send format data request: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_format_data_response( extern "C" fn client_format_data_response(
@@ -1125,9 +1138,13 @@ extern "C" fn client_format_data_response(
msg_flags, msg_flags,
format_data, format_data,
}; };
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send format data response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_file_contents_request( extern "C" fn client_file_contents_request(
@@ -1175,9 +1192,13 @@ extern "C" fn client_file_contents_request(
clip_data_id, clip_data_id,
}; };
log::debug!("client_file_contents_request called, data: {:?}", &data); log::debug!("client_file_contents_request called, data: {:?}", &data);
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send file contents request: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }
extern "C" fn client_file_contents_response( extern "C" fn client_file_contents_response(
@@ -1213,7 +1234,11 @@ extern "C" fn client_file_contents_response(
msg_flags, msg_flags,
stream_id stream_id
); );
send_data(conn_id, data); match send_data(conn_id, data) {
Ok(_) => 0,
0 Err(e) => {
log::error!("failed to send file contents response: {:?}", e);
ERR_CODE_SEND_MSG
}
}
} }

View File

@@ -220,7 +220,8 @@ struct wf_clipboard
HWND hwnd; HWND hwnd;
HANDLE hmem; HANDLE hmem;
HANDLE thread; HANDLE thread;
HANDLE response_data_event; HANDLE formatDataRespEvent;
BOOL formatDataRespReceived;
LPDATAOBJECT data_obj; LPDATAOBJECT data_obj;
HANDLE data_obj_mutex; HANDLE data_obj_mutex;
@@ -228,6 +229,7 @@ struct wf_clipboard
ULONG req_fsize; ULONG req_fsize;
char *req_fdata; char *req_fdata;
HANDLE req_fevent; HANDLE req_fevent;
BOOL req_f_received;
size_t nFiles; size_t nFiles;
size_t file_array_size; 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, static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid,
void **ppvObject) void **ppvObject)
{ {
if (ppvObject == NULL)
return E_INVALIDARG;
if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown))
{ {
IStream_AddRef(This); IStream_AddRef(This);
@@ -362,6 +367,13 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
} }
*pcbRead = clipboard->req_fsize; *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; instance->m_lOffset.QuadPart += clipboard->req_fsize;
if (clipboard->req_fsize < cb) 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) static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc)
{ {
IStream *iStream; IStream *iStream = NULL;
BOOL success = FALSE; BOOL success = FALSE;
BOOL isDir = FALSE; BOOL isDir = FALSE;
CliprdrStream *instance; CliprdrStream *instance = NULL;
wfClipboard *clipboard = (wfClipboard *)pData; wfClipboard *clipboard = (wfClipboard *)pData;
if (!(pData && dsc))
{
return NULL;
}
instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream));
if (instance) 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, static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count,
void *data) void *data)
{ {
CliprdrDataObject *instance; CliprdrDataObject *instance = NULL;
IDataObject *iDataObject; IDataObject *iDataObject = NULL;
instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject));
if (!instance) if (!instance)
goto error; goto error;
instance->m_pFormatEtc = NULL;
instance->m_pStgMedium = NULL;
iDataObject = &instance->iDataObject; iDataObject = &instance->iDataObject;
iDataObject->lpVtbl = NULL;
iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl));
if (!iDataObject->lpVtbl) if (!iDataObject->lpVtbl)
@@ -929,7 +951,24 @@ static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc
return instance; return instance;
error: 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; return NULL;
} }
@@ -1010,6 +1049,8 @@ static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMAT
REFIID riid, void **ppvObject) REFIID riid, void **ppvObject)
{ {
(void)This; (void)This;
if (!ppvObject)
return E_INVALIDARG;
if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) 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; WCHAR *unicode_name;
#if !defined(UNICODE) #if !defined(UNICODE)
size_t size; size_t size;
int towchar_count;
#endif #endif
if (!clipboard || !format_name) if (!clipboard || !format_name)
@@ -1205,6 +1247,8 @@ static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *f
#if defined(UNICODE) #if defined(UNICODE)
unicode_name = _wcsdup(format_name); unicode_name = _wcsdup(format_name);
if (!unicode_name)
return 0;
#else #else
size = _tcslen(format_name); size = _tcslen(format_name);
unicode_name = calloc(size + 1, sizeof(WCHAR)); 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) if (!unicode_name)
return 0; return 0;
MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0);
#endif if (towchar_count <= 0 || towchar_count > size)
if (!unicode_name)
return 0; 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++) for (i = 0; i < clipboard->map_size; i++)
{ {
@@ -1312,6 +1358,9 @@ static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
if (!clipboard) if (!clipboard)
return -1; 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)) == if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) ==
0) 0)
return -1; return -1;
@@ -1444,7 +1493,37 @@ static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID)
return rc; 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; UINT rc = ERROR_SUCCESS;
clipboard->context->IsStopped = FALSE; clipboard->context->IsStopped = FALSE;
@@ -1456,7 +1535,21 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis); DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis);
if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE) 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) if (clipboard->context->IsStopped == TRUE)
@@ -1470,12 +1563,6 @@ UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, vo
return ERROR_INTERNAL_ERROR; return ERROR_INTERNAL_ERROR;
} }
if (!ResetEvent(event))
{
// NOTE: critical error here, crash may be better
rc = ERROR_INTERNAL_ERROR;
}
if ((*data) == NULL) if ((*data) == NULL)
{ {
rc = ERROR_INTERNAL_ERROR; 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) if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest)
return ERROR_INTERNAL_ERROR; 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); remoteFormatId = get_remote_format_id(clipboard, formatId);
formatDataRequest.connID = connID; formatDataRequest.connID = connID;
@@ -1530,7 +1624,7 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
return rc; 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, 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) if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
return ERROR_INTERNAL_ERROR; 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; 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.streamId = (UINT32)(ULONG_PTR)streamid;
fileContentsRequest.listIndex = index; fileContentsRequest.listIndex = index;
fileContentsRequest.dwFlags = flag; fileContentsRequest.dwFlags = flag;
@@ -1558,7 +1662,7 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
return rc; 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( static UINT cliprdr_send_response_filecontents(
@@ -1788,6 +1892,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
break; break;
case WM_DESTROYCLIPBOARD: case WM_DESTROYCLIPBOARD:
// to-do: clear clipboard data?
case WM_ASKCBFORMATNAME: case WM_ASKCBFORMATNAME:
case WM_HSCROLLCLIPBOARD: case WM_HSCROLLCLIPBOARD:
case WM_PAINTCLIPBOARD: 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) LONG positionHigh, DWORD nRequested, DWORD *puSize)
{ {
BOOL res = FALSE; BOOL res = FALSE;
HANDLE hFile; HANDLE hFile = NULL;
DWORD nGet, rc; DWORD nGet, rc;
if (!file_name || !buffer || !puSize) 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; res = TRUE;
error: error:
if (hFile)
if (!CloseHandle(hFile)) {
res = FALSE; if (!CloseHandle(hFile))
res = FALSE;
}
if (res) if (res)
*puSize = nGet; *puSize = nGet;
@@ -1945,8 +2052,8 @@ error:
/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ /* 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) static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen)
{ {
HANDLE hFile; HANDLE hFile = NULL;
FILEDESCRIPTORW *fd; FILEDESCRIPTORW *fd = NULL;
fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW));
if (!fd) if (!fd)
@@ -1975,7 +2082,16 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
} }
fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); 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); CloseHandle(hFile);
return fd; 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]) if (!clipboard->file_names[clipboard->nFiles])
return FALSE; 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 */ /* add to descriptor array */
clipboard->fileDescriptor[clipboard->nFiles] = clipboard->fileDescriptor[clipboard->nFiles] =
wf_cliprdr_get_file_descriptor(full_file_name, pathLen); 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) if (!clipboard || !Dir)
return FALSE; return FALSE;
// StringCchCopy(DirSpec, MAX_PATH, Dir); if (wcslen(Dir) + 3 > MAX_PATH)
// StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); return FALSE;
StringCchCopyW(DirSpec, MAX_PATH, Dir); StringCchCopyW(DirSpec, MAX_PATH, Dir);
StringCchCatW(DirSpec, MAX_PATH, L"\\*"); 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) if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{ {
WCHAR DirAdd[MAX_PATH]; WCHAR DirAdd[MAX_PATH];
// StringCchCopy(DirAdd, MAX_PATH, Dir); if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
// StringCchCat(DirAdd, MAX_PATH, _T("\\")); return FALSE;
// StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName);
StringCchCopyW(DirAdd, MAX_PATH, Dir); StringCchCopyW(DirAdd, MAX_PATH, Dir);
StringCchCatW(DirAdd, MAX_PATH, L"\\"); StringCchCatW(DirAdd, MAX_PATH, L"\\");
StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName);
@@ -2094,10 +2214,8 @@ static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, si
else else
{ {
WCHAR fileName[MAX_PATH]; WCHAR fileName[MAX_PATH];
// StringCchCopy(fileName, MAX_PATH, Dir); if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH)
// StringCchCat(fileName, MAX_PATH, _T("\\")); return FALSE;
// StringCchCat(fileName, MAX_PATH, FindFileData.cFileName);
StringCchCopyW(fileName, MAX_PATH, Dir); StringCchCopyW(fileName, MAX_PATH, Dir);
StringCchCatW(fileName, MAX_PATH, L"\\"); StringCchCatW(fileName, MAX_PATH, L"\\");
StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName);
@@ -2242,9 +2360,11 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
if (context->EnableFiles) if (context->EnableFiles)
{ {
UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32)); UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32));
*p_conn_id = formatList->connID; if (p_conn_id) {
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) *p_conn_id = formatList->connID;
rc = CHANNEL_RC_OK; if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id))
rc = CHANNEL_RC_OK;
}
} }
else else
{ {
@@ -2265,16 +2385,30 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
// SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL);
FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS));
format_ids->connID = formatList->connID; if (format_ids)
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)
{ {
format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; format_ids->connID = formatList->connID;
} format_ids->size = (UINT32)clipboard->map_size;
if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32));
{ if (format_ids->formats)
rc = CHANNEL_RC_OK; {
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 else
{ {
@@ -2469,17 +2603,28 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
p += len + 1, clipboard->nFiles++) p += len + 1, clipboard->nFiles++)
{ {
int cchWideChar; int cchWideChar;
WCHAR *wFileName;
cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0);
wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR));
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); if (wFileName)
wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); {
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); GlobalUnlock(stg_medium.hGlobal);
ReleaseStgMedium(&stg_medium); ReleaseStgMedium(&stg_medium);
resp: resp:
// size will not overflow, because size type is size_t (unsigned __int64)
size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW);
groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size);
@@ -2519,10 +2664,17 @@ wf_cliprdr_server_format_data_request(CliprdrClientContext *context,
globlemem = (char *)GlobalLock(hClipdata); globlemem = (char *)GlobalLock(hClipdata);
size = (int)GlobalSize(hClipdata); size = (int)GlobalSize(hClipdata);
buff = malloc(size); buff = malloc(size);
CopyMemory(buff, globlemem, size); if (buff)
{
CopyMemory(buff, globlemem, size);
rc = ERROR_SUCCESS;
}
else
{
rc = ERROR_INTERNAL_ERROR;
}
GlobalUnlock(hClipdata); GlobalUnlock(hClipdata);
CloseClipboard(); CloseClipboard();
rc = ERROR_SUCCESS;
} }
} }
else else
@@ -2545,7 +2697,7 @@ exit:
response.requestedFormatData = (BYTE *)buff; response.requestedFormatData = (BYTE *)buff;
if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response)) 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) if (buff)
@@ -2621,9 +2773,11 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
rc = CHANNEL_RC_OK; rc = CHANNEL_RC_OK;
} while (0); } 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; rc = ERROR_INTERNAL_ERROR;
} }
return rc; return rc;
@@ -2899,7 +3053,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
if (!SetEvent(clipboard->req_fevent)) 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; return rc;
} }
@@ -2934,14 +3090,16 @@ BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
(formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping))))
goto error; goto error;
if (!(clipboard->response_data_event = CreateEvent(NULL, TRUE, FALSE, NULL))) if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL)))
goto error; goto error;
clipboard->formatDataRespReceived = FALSE;
if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex"))) if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex")))
goto error; goto error;
if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL)))
goto error; goto error;
clipboard->req_f_received = FALSE;
if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL)))
goto error; goto error;
@@ -3002,8 +3160,8 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
clipboard->data_obj = NULL; clipboard->data_obj = NULL;
} }
if (clipboard->response_data_event) if (clipboard->formatDataRespEvent)
CloseHandle(clipboard->response_data_event); CloseHandle(clipboard->formatDataRespEvent);
if (clipboard->data_obj_mutex) if (clipboard->data_obj_mutex)
CloseHandle(clipboard->data_obj_mutex); CloseHandle(clipboard->data_obj_mutex);

View File

@@ -37,6 +37,9 @@ const kUCKeyActionDisplay: u16 = 3;
const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31;
const BUF_LEN: usize = 4; 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. /// The event source user data value of cgevent.
pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100; pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100;
@@ -226,14 +229,24 @@ impl MouseControllable for Enigo {
} }
self.last_click_time = Some(now); self.last_click_time = Some(now);
let (current_x, current_y) = Self::mouse_location(); let (current_x, current_y) = Self::mouse_location();
let (button, event_type) = match button { let (button, event_type, btn_value) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), 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); log::info!("Unsupported button {:?}", button);
return Ok(()); return Ok(());
}, }
}; };
let dest = CGPoint::new(current_x as f64, current_y as f64); let dest = CGPoint::new(current_x as f64, current_y as f64);
if let Some(src) = self.event_source.as_ref() { if let Some(src) = self.event_source.as_ref() {
@@ -244,6 +257,9 @@ impl MouseControllable for Enigo {
self.multiple_click, self.multiple_click,
); );
} }
if let Some(v) = btn_value {
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
}
self.post(event); self.post(event);
} }
} }
@@ -252,14 +268,24 @@ impl MouseControllable for Enigo {
fn mouse_up(&mut self, button: MouseButton) { fn mouse_up(&mut self, button: MouseButton) {
let (current_x, current_y) = Self::mouse_location(); let (current_x, current_y) = Self::mouse_location();
let (button, event_type) = match button { let (button, event_type, btn_value) = match button {
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None),
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None),
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), 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); log::info!("Unsupported button {:?}", button);
return; return;
}, }
}; };
let dest = CGPoint::new(current_x as f64, current_y as f64); let dest = CGPoint::new(current_x as f64, current_y as f64);
if let Some(src) = self.event_source.as_ref() { if let Some(src) = self.event_source.as_ref() {
@@ -270,6 +296,9 @@ impl MouseControllable for Enigo {
self.multiple_click, self.multiple_click,
); );
} }
if let Some(v) = btn_value {
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v);
}
self.post(event); self.post(event);
} }
} }
@@ -345,7 +374,7 @@ impl KeyboardControllable for Enigo {
fn as_mut_any(&mut self) -> &mut dyn std::any::Any { fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
self self
} }
fn key_sequence(&mut self, sequence: &str) { fn key_sequence(&mut self, sequence: &str) {
// NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 // 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 // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time
@@ -382,12 +411,10 @@ impl KeyboardControllable for Enigo {
fn key_down(&mut self, key: Key) -> crate::ResultType { fn key_down(&mut self, key: Key) -> crate::ResultType {
let code = self.key_to_keycode(key); let code = self.key_to_keycode(key);
if code == u16::MAX { if code == u16::MAX {
return Err("".into()); return Err("".into());
} }
if let Some(src) = self.event_source.as_ref() { if let Some(src) = self.event_source.as_ref() {
if let Ok(event) = if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) {
CGEvent::new_keyboard_event(src.clone(), code, true)
{
self.post(event); self.post(event);
} }
} }

View File

@@ -326,6 +326,7 @@ enum ClipboardFormat {
ImageRgba = 21; ImageRgba = 21;
ImagePng = 22; ImagePng = 22;
ImageSvg = 23; ImageSvg = 23;
Special = 31;
} }
message Clipboard { message Clipboard {
@@ -334,6 +335,8 @@ message Clipboard {
int32 width = 3; int32 width = 3;
int32 height = 4; int32 height = 4;
ClipboardFormat format = 5; ClipboardFormat format = 5;
// Special format name, only used when format is Special.
string special_name = 6;
} }
message MultiClipboards { repeated Clipboard clipboards = 1; } message MultiClipboards { repeated Clipboard clipboards = 1; }

View File

@@ -39,7 +39,7 @@ pub const REG_INTERVAL: i64 = 15_000;
pub const COMPRESS_LEVEL: i32 = 3; pub const COMPRESS_LEVEL: i32 = 3;
const SERIAL: i32 = 3; const SERIAL: i32 = 3;
const PASSWORD_ENC_VERSION: &str = "00"; 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")] #[cfg(target_os = "macos")]
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -296,6 +296,8 @@ pub struct PeerConfig {
pub keyboard_mode: String, pub keyboard_mode: String,
#[serde(flatten)] #[serde(flatten)]
pub view_only: ViewOnly, pub view_only: ViewOnly,
#[serde(flatten)]
pub sync_init_clipboard: SyncInitClipboard,
// Mouse wheel or touchpad scroll mode // Mouse wheel or touchpad scroll mode
#[serde( #[serde(
default = "PeerConfig::default_reverse_mouse_wheel", default = "PeerConfig::default_reverse_mouse_wheel",
@@ -373,6 +375,7 @@ impl Default for PeerConfig {
ui_flutter: Default::default(), ui_flutter: Default::default(),
info: Default::default(), info: Default::default(),
transfer: Default::default(), transfer: Default::default(),
sync_init_clipboard: Default::default(),
} }
} }
} }
@@ -1462,6 +1465,13 @@ serde_field_bool!(
"ViewOnly::default_view_only" "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)] #[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct LocalConfig { pub struct LocalConfig {
#[serde(default, deserialize_with = "deserialize_string")] #[serde(default, deserialize_with = "deserialize_string")]
@@ -2156,6 +2166,7 @@ pub mod keys {
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; 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_THEME: &str = "theme";
pub const OPTION_LANGUAGE: &str = "lang"; pub const OPTION_LANGUAGE: &str = "lang";
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
@@ -2218,6 +2229,9 @@ pub mod keys {
pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards";
pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password";
pub const OPTION_HIDE_TRAY: &str = "hide-tray"; 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 // flutter local options
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
@@ -2276,6 +2290,7 @@ pub mod keys {
OPTION_CUSTOM_IMAGE_QUALITY, OPTION_CUSTOM_IMAGE_QUALITY,
OPTION_CUSTOM_FPS, OPTION_CUSTOM_FPS,
OPTION_CODEC_PREFERENCE, OPTION_CODEC_PREFERENCE,
OPTION_SYNC_INIT_CLIPBOARD,
]; ];
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS // DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
@@ -2362,6 +2377,9 @@ pub mod keys {
OPTION_HIDE_HELP_CARDS, OPTION_HIDE_HELP_CARDS,
OPTION_DEFAULT_CONNECT_PASSWORD, OPTION_DEFAULT_CONNECT_PASSWORD,
OPTION_HIDE_TRAY, OPTION_HIDE_TRAY,
OPTION_ONE_WAY_CLIPBOARD_REDIRECTION,
OPTION_ALLOW_LOGON_SCREEN_PASSWORD,
OPTION_ONE_WAY_FILE_TRANSFER,
]; ];
} }

View File

@@ -89,11 +89,11 @@ pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String
log::error!("Duplicate encryption!"); log::error!("Duplicate encryption!");
return s.to_owned(); return s.to_owned();
} }
if s.bytes().len() > max_len { if s.chars().count() > max_len {
return String::default(); return String::default();
} }
if version == "00" { 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; return version.to_owned() + &s;
} }
} }
@@ -130,7 +130,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u
return vec![]; return vec![];
} }
if version == "00" { if version == "00" {
if let Ok(s) = encrypt(v, max_len) { if let Ok(s) = encrypt(v) {
let mut version = version.to_owned().into_bytes(); let mut version = version.to_owned().into_bytes();
version.append(&mut s.into_bytes()); version.append(&mut s.into_bytes());
return version; return version;
@@ -155,8 +155,8 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, boo
(v.to_owned(), false, !v.is_empty()) (v.to_owned(), false, !v.is_empty())
} }
fn encrypt(v: &[u8], max_len: usize) -> Result<String, ()> { fn encrypt(v: &[u8]) -> Result<String, ()> {
if !v.is_empty() && v.len() <= max_len { if !v.is_empty() {
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
} else { } else {
Err(()) Err(())

View File

@@ -7,6 +7,9 @@ lazy_static::lazy_static! {
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
pub const DISPLAY_SERVER_X11: &str = "x11"; 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 struct Distro {
pub name: String, 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] #[inline]
pub fn is_gdm_user(username: &str) -> bool { pub fn is_gdm_user(username: &str) -> bool {
username == "gdm" username == "gdm"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.3.0" version = "1.3.2"
edition = "2021" edition = "2021"
description = "RustDesk Remote Desktop" description = "RustDesk Remote Desktop"

View File

@@ -498,6 +498,15 @@ pub struct HwCodecConfig {
pub vram_decode: Vec<hwcodec::vram::DecodeContext>, pub vram_decode: Vec<hwcodec::vram::DecodeContext>,
} }
// 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<A>, b: Vec<String>} will fail if b is empty, but struct T { a: Vec<String>, b: Vec<String>} 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 // 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 // install: --server start check process, check process send to --server, ui get from --server
// portable: ui start check process, check process send to ui // portable: ui start check process, check process send to ui
@@ -509,7 +518,12 @@ impl HwCodecConfig {
log::info!("set hwcodec config"); log::info!("set hwcodec config");
log::debug!("{config:?}"); log::debug!("{config:?}");
#[cfg(any(windows, target_os = "macos"))] #[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.lock().unwrap() = Some(config);
*CONFIG_SET_BY_IPC.lock().unwrap() = true; *CONFIG_SET_BY_IPC.lock().unwrap() = true;
} }
@@ -587,7 +601,8 @@ impl HwCodecConfig {
Some(c) => c, Some(c) => c,
None => { None => {
log::info!("try load cached hwcodec config"); log::info!("try load cached hwcodec config");
let c = hbb_common::config::common_load::<HwCodecConfig>("_hwcodec"); let c = hbb_common::config::common_load::<HwCodecConfig2>("_hwcodec");
let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default();
let new_signature = hwcodec::common::get_gpu_signature(); let new_signature = hwcodec::common::get_gpu_signature();
if c.signature == new_signature { if c.signature == new_signature {
log::debug!("load cached hwcodec config: {c:?}"); log::debug!("load cached hwcodec config: {c:?}");

View File

@@ -316,7 +316,7 @@ impl ToString for CodecFormat {
CodecFormat::AV1 => "AV1".into(), CodecFormat::AV1 => "AV1".into(),
CodecFormat::H264 => "H264".into(), CodecFormat::H264 => "H264".into(),
CodecFormat::H265 => "H265".into(), CodecFormat::H265 => "H265".into(),
CodecFormat::Unknown => "Unknow".into(), CodecFormat::Unknown => "Unknown".into(),
} }
} }
} }

View File

@@ -27,39 +27,40 @@ use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_porta
use lazy_static::lazy_static; use lazy_static::lazy_static;
lazy_static! { lazy_static! {
pub static ref RDP_RESPONSE: Mutex<Option<RdpResponse>> = Mutex::new(None); pub static ref RDP_SESSION_INFO: Mutex<Option<RdpSessionInfo>> = Mutex::new(None);
} }
#[inline] #[inline]
pub fn close_session() { pub fn close_session() {
let _ = RDP_RESPONSE.lock().unwrap().take(); let _ = RDP_SESSION_INFO.lock().unwrap().take();
} }
#[inline] #[inline]
pub fn is_rdp_session_hold() -> bool { 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() { 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; 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 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; close = true;
} }
} }
if close { if close {
*rdp_res = None; *rdp_info = None;
} }
} }
pub struct RdpResponse { pub struct RdpSessionInfo {
pub conn: Arc<SyncConnection>, pub conn: Arc<SyncConnection>,
pub streams: Vec<PwStreamInfo>, pub streams: Vec<PwStreamInfo>,
pub fd: OwnedFd, pub fd: OwnedFd,
pub session: dbus::Path<'static>, pub session: dbus::Path<'static>,
pub is_support_restore_token: bool, pub is_support_restore_token: bool,
pub resolution: Arc<Mutex<Option<(usize, usize)>>>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct PwStreamInfo { pub struct PwStreamInfo {
@@ -69,6 +70,12 @@ pub struct PwStreamInfo {
size: (usize, usize), size: (usize, usize),
} }
impl PwStreamInfo {
pub fn get_size(&self) -> (usize, usize) {
self.size
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct DBusError(String); pub struct DBusError(String);
@@ -105,24 +112,31 @@ pub struct PipeWireCapturable {
} }
impl PipeWireCapturable { impl PipeWireCapturable {
fn new(conn: Arc<SyncConnection>, fd: OwnedFd, stream: PwStreamInfo) -> Self { fn new(
conn: Arc<SyncConnection>,
fd: OwnedFd,
resolution: Arc<Mutex<Option<(usize, usize)>>>,
stream: PwStreamInfo,
) -> Self {
// alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling // 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 // https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244
let res = get_res(Self { let size = get_res(Self {
dbus_conn: conn.clone(), dbus_conn: conn.clone(),
fd: fd.clone(), fd: fd.clone(),
path: stream.path, path: stream.path,
source_type: stream.source_type, source_type: stream.source_type,
position: stream.position, position: stream.position,
size: stream.size, size: stream.size,
}); })
.unwrap_or(stream.size);
*resolution.lock().unwrap() = Some(size);
Self { Self {
dbus_conn: conn, dbus_conn: conn,
fd, fd,
path: stream.path, path: stream.path,
source_type: stream.source_type, source_type: stream.source_type,
position: stream.position, position: stream.position,
size: res.unwrap_or(stream.size), size,
} }
} }
} }
@@ -813,7 +827,7 @@ fn on_start_response(
} }
pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> { pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
let mut rdp_connection = match RDP_RESPONSE.lock() { let mut rdp_connection = match RDP_SESSION_INFO.lock() {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => return Err(Box::new(err)), Err(err) => return Err(Box::new(err)),
}; };
@@ -822,28 +836,36 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?; let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
let conn = Arc::new(conn); let conn = Arc::new(conn);
let rdp_res = RdpResponse { let rdp_info = RdpSessionInfo {
conn, conn,
streams, streams,
fd, fd,
session, session,
is_support_restore_token, 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, Some(res) => res,
None => { None => {
return Err(Box::new(DBusError("RDP response is None.".into()))); return Err(Box::new(DBusError("RDP response is None.".into())));
} }
}; };
Ok(rdp_res Ok(rdp_info
.streams .streams
.clone() .clone()
.into_iter() .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()) .collect())
} }

View File

@@ -1,3 +1,4 @@
use hbb_common::libc;
use std::ptr; use std::ptr;
use std::rc::Rc; 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() { if reply.is_null() {
// TODO: Should seperate SHM disabled from SHM not supported? // TODO: Should seperate SHM disabled from SHM not supported?
return Err(Error::UnsupportedExtension); return Err(Error::UnsupportedExtension);
} else if e.is_null() {
return Ok(());
} else { } else {
// TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? // https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229
return Err(Error::Generic); 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);
}
} }
} }

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk pkgname=rustdesk
pkgver=1.3.0 pkgver=1.3.2
pkgrel=0 pkgrel=0
epoch= epoch=
pkgdesc="" pkgdesc=""

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>RustDesk</vendor>
<vendor_url>https://rustdesk.com/</vendor_url>
<icon_name>rustdesk</icon_name>
<action id="com.rustdesk.RustDesk.options">
<description>Change RustDesk options</description>
<message>Authentication is required to change RustDesk options</message>
<message xml:lang="zh_CN">要更改RustDesk选项, 需要您先通过身份验证</message>
<message xml:lang="zh_TW">要變更RustDesk選項, 需要您先通過身份驗證</message>
<message xml:lang="de">Authentifizierung zum Ändern der RustDesk-Optionen</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/share/rustdesk/files/polkit</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>

View File

@@ -1,9 +1,10 @@
Name: rustdesk Name: rustdesk
Version: 1.3.0 Version: 1.3.2
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 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) 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 %description

View File

@@ -1,9 +1,10 @@
Name: rustdesk Name: rustdesk
Version: 1.3.0 Version: 1.3.2
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 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) 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 %description

View File

@@ -3,7 +3,8 @@ Version: 1.1.9
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 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 %description
The best open-source remote desktop client software, written in Rust. The best open-source remote desktop client software, written in Rust.

View File

@@ -1,9 +1,10 @@
Name: rustdesk Name: rustdesk
Version: 1.3.0 Version: 1.3.2
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 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 %description
The best open-source remote desktop client software, written in Rust. The best open-source remote desktop client software, written in Rust.

View File

@@ -1,95 +0,0 @@
From afe89a70f6bc7ebd0a6a0a31101801b88cbd60ee Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com>
Date: Sun, 5 May 2024 12:45:23 +0800
Subject: [PATCH] use release/7.0's update_bitrate
Signed-off-by: 21pages <pages21@163.com>
---
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

View File

@@ -1,40 +0,0 @@
From be3d9d8092720bbe4239212648d2e9c4ffd7f40c Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com>
Date: Wed, 22 May 2024 17:09:28 +0800
Subject: [PATCH] android mediacodec encode align 64
Signed-off-by: 21pages <pages21@163.com>
---
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

View File

@@ -1,9 +1,9 @@
From f0b694749b38b2cfd94df4eed10e667342c234e5 Mon Sep 17 00:00:00 2001 From f6988e5424e041ff6f6e241f4d8fa69a04c05e64 Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com> From: 21pages <sunboeasy@gmail.com>
Date: Sat, 24 Feb 2024 15:33:24 +0800 Date: Thu, 5 Sep 2024 16:26:20 +0800
Subject: [PATCH 1/2] avcodec/amfenc: add query_timeout option for h264/hevc Subject: [PATCH 1/3] avcodec/amfenc: add query_timeout option for h264/hevc
Signed-off-by: 21pages <pages21@163.com> Signed-off-by: 21pages <sunboeasy@gmail.com>
--- ---
libavcodec/amfenc.h | 1 + libavcodec/amfenc.h | 1 +
libavcodec/amfenc_h264.c | 4 ++++ libavcodec/amfenc_h264.c | 4 ++++
@@ -11,10 +11,10 @@ Signed-off-by: 21pages <pages21@163.com>
3 files changed, 9 insertions(+) 3 files changed, 9 insertions(+)
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index 1ab98d2f78..e92120ea39 100644 index 2dbd378ef8..d636673a9d 100644
--- a/libavcodec/amfenc.h --- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h
@@ -87,6 +87,7 @@ typedef struct AmfContext { @@ -89,6 +89,7 @@ typedef struct AmfContext {
int quality; int quality;
int b_frame_delta_qp; int b_frame_delta_qp;
int ref_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 // Dynamic options, can be set after Init() call
diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c 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 --- a/libavcodec/amfenc_h264.c
+++ b/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 }, { "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 }, { "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 }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE },
{ NULL } //Pre Analysis options
}; { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE },
@@ -155,6 +156,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) @@ -222,6 +223,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate);
+ if (ctx->query_timeout >= 0) + 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) { switch (avctx->profile) {
case FF_PROFILE_H264_BASELINE: case AV_PROFILE_H264_BASELINE:
profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE;
diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c 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 --- a/libavcodec/amfenc_hevc.c
+++ b/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 }, { "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 }, { "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 }, + { "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); 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); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout);
+ +
switch (avctx->profile) { switch (avctx->profile) {
case FF_PROFILE_HEVC_MAIN: case AV_PROFILE_HEVC_MAIN:
profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN;
-- --
2.43.0.windows.1 2.43.0.windows.1

View File

@@ -1,16 +1,16 @@
From 4d0d20d96ad458cfec0444b9be0182ca6085ee0c Mon Sep 17 00:00:00 2001 From 6e76c57cf2c0e790228f19c88089eef110fd74aa Mon Sep 17 00:00:00 2001
From: 21pages <pages21@163.com> From: 21pages <sunboeasy@gmail.com>
Date: Sat, 24 Feb 2024 16:02:44 +0800 Date: Thu, 5 Sep 2024 16:32:16 +0800
Subject: [PATCH 2/2] libavcodec/amfenc: reconfig when bitrate change Subject: [PATCH 2/3] libavcodec/amfenc: reconfig when bitrate change
Signed-off-by: 21pages <pages21@163.com> Signed-off-by: 21pages <sunboeasy@gmail.com>
--- ---
libavcodec/amfenc.c | 20 ++++++++++++++++++++ libavcodec/amfenc.c | 20 ++++++++++++++++++++
libavcodec/amfenc.h | 1 + libavcodec/amfenc.h | 1 +
2 files changed, 21 insertions(+) 2 files changed, 21 insertions(+)
diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c
index a033e1220e..3eab01a903 100644 index 061859f85c..97587fe66b 100644
--- a/libavcodec/amfenc.c --- a/libavcodec/amfenc.c
+++ b/libavcodec/amfenc.c +++ b/libavcodec/amfenc.c
@@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx) @@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx)
@@ -21,7 +21,7 @@ index a033e1220e..3eab01a903 100644
// configure AMF logger // configure AMF logger
// the return of these functions indicates old state and do not affect behaviour // 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); 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) int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
{ {
AmfContext *ctx = avctx->priv_data; AmfContext *ctx = avctx->priv_data;
@@ -586,6 +604,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) @@ -596,6 +614,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
AVFrame *frame = ctx->delayed_frame; int query_output_data_flag = 0;
int block_and_wait; AMF_RESULT res_resubmit;
+ reconfig_encoder(avctx); + reconfig_encoder(avctx);
+ +
@@ -55,13 +55,13 @@ index a033e1220e..3eab01a903 100644
return AVERROR(EINVAL); return AVERROR(EINVAL);
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index e92120ea39..31172645f2 100644 index d636673a9d..09506ee2e0 100644
--- a/libavcodec/amfenc.h --- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h
@@ -107,6 +107,7 @@ typedef struct AmfContext { @@ -113,6 +113,7 @@ typedef struct AmfContext {
int me_half_pel; int max_b_frames;
int me_quarter_pel; int qvbr_quality_level;
int aud; int hw_high_motion_quality_boost;
+ int64_t av_bitrate; + int64_t av_bitrate;
// HEVC - specific options // HEVC - specific options

View File

@@ -1,32 +1,32 @@
From 8fd62e4ecd058b09abf8847be5fbbf0eef44a90f Mon Sep 17 00:00:00 2001 From 14b77216106eaaff9cf701528039ae4264eaf420 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com> From: 21pages <sunboeasy@gmail.com>
Date: Tue, 16 Jul 2024 14:58:33 +0800 Date: Thu, 5 Sep 2024 16:41:59 +0800
Subject: [PATCH] amf colorspace Subject: [PATCH 3/3] amf colorspace
Signed-off-by: 21pages <sunboeasy@gmail.com> Signed-off-by: 21pages <sunboeasy@gmail.com>
--- ---
libavcodec/amfenc.h | 1 + libavcodec/amfenc.h | 1 +
libavcodec/amfenc_h264.c | 39 +++++++++++++++++++++++++++++++++ libavcodec/amfenc_h264.c | 40 ++++++++++++++++++++++++++++++++++
libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++ 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 diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index 31172645f2..493e01603d 100644 index 09506ee2e0..7f458b14f7 100644
--- a/libavcodec/amfenc.h --- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h
@@ -23,6 +23,7 @@ @@ -24,6 +24,7 @@
#include <AMF/components/VideoEncoderVCE.h> #include <AMF/components/VideoEncoderVCE.h>
#include <AMF/components/VideoEncoderHEVC.h> #include <AMF/components/VideoEncoderHEVC.h>
#include <AMF/components/VideoEncoderAV1.h>
+#include <AMF/components/ColorSpace.h> +#include <AMF/components/ColorSpace.h>
#include "libavutil/fifo.h" #include "libavutil/fifo.h"
diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c 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 --- a/libavcodec/amfenc_h264.c
+++ b/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; AMFRate framerate;
AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); AMFSize framesize = AMFConstructSize(avctx->width, avctx->height);
int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; 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) { if (avctx->framerate.num > 0 && avctx->framerate.den > 0) {
framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); 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); 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; + color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;
+ break; + break;
+ } + }
+ } }
+ pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; + pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt;
+ color_depth = AMF_COLOR_BIT_DEPTH_8; + color_depth = AMF_COLOR_BIT_DEPTH_8;
+ if (pix_fmt == AV_PIX_FMT_P010) { + if (pix_fmt == AV_PIX_FMT_P010) {
+ color_depth = AMF_COLOR_BIT_DEPTH_10; + 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_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_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_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); + AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries);
// autodetect rate control method // autodetect rate control method
if (ctx->rate_control_mode == AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_UNKNOWN) { 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 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 --- a/libavcodec/amfenc_hevc.c
+++ b/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; AMFRate framerate;
AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); AMFSize framesize = AMFConstructSize(avctx->width, avctx->height);
int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; 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) { if (avctx->framerate.num > 0 && avctx->framerate.den > 0) {
framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den);
@@ -130,6 +133,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) @@ -191,6 +194,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
case FF_PROFILE_HEVC_MAIN: case AV_PROFILE_HEVC_MAIN:
profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN;
break; break;
+ case FF_PROFILE_HEVC_MAIN_10: + case AV_PROFILE_HEVC_MAIN_10:
+ profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10; + profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10;
+ break; + break;
default: default:
break; 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); AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_ASPECT_RATIO, ratio);
} }

View File

@@ -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( vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH OUT_SOURCE_PATH SOURCE_PATH
REPO ffmpeg/ffmpeg REPO ffmpeg/ffmpeg
REF "${FF_VERSION}" REF "n${VERSION}"
SHA512 "${FF_SHA512}" SHA512 3ba02e8b979c80bf61d55f414bdac2c756578bb36498ed7486151755c6ccf8bd8ff2b8c7afa3c5d1acd862ce48314886a86a105613c05e36601984c334f8f6bf
HEAD_REF master HEAD_REF master
PATCHES PATCHES
0002-fix-msvc-link.patch # upstreamed in future version 0002-fix-msvc-link.patch # upstreamed in future version
@@ -18,25 +10,11 @@ vcpkg_from_github(
0005-fix-nasm.patch # upstreamed in future version 0005-fix-nasm.patch # upstreamed in future version
0012-Fix-ssl-110-detection.patch 0012-Fix-ssl-110-detection.patch
0013-define-WINVER.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 " ") if(SOURCE_PATH MATCHES " ")
message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces") message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces")
endif() endif()
@@ -130,6 +108,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
string(APPEND OPTIONS "\ string(APPEND OPTIONS "\
--target-os=win32 \ --target-os=win32 \
--toolchain=msvc \ --toolchain=msvc \
--cc=cl \
--enable-gpl \ --enable-gpl \
--enable-d3d11va \ --enable-d3d11va \
--enable-cuda \ --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_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"")
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -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 # # Setup vcpkg toolchain
set(prog_env "") set(prog_env "")
@@ -219,8 +202,9 @@ if(VCPKG_DETECTED_CMAKE_C_COMPILER)
get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME) get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME)
set(ENV{CC} "${CC_filename}") set(ENV{CC} "${CC_filename}")
string(APPEND OPTIONS " --cc=${CC_filename}") string(APPEND OPTIONS " --cc=${CC_filename}")
if(VCPKG_HOST_IS_WINDOWS)
# string(APPEND OPTIONS " --host_cc=${CC_filename}") ffmpeg not yet setup for cross builds? string(APPEND OPTIONS " --host_cc=${CC_filename}")
endif()
list(APPEND prog_env "${CC_path}") list(APPEND prog_env "${CC_path}")
endif() endif()
@@ -291,6 +275,13 @@ if(VCPKG_DETECTED_CMAKE_STRIP)
list(APPEND prog_env "${STRIP_path}") list(APPEND prog_env "${STRIP_path}")
endif() 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) list(REMOVE_DUPLICATES prog_env)
vcpkg_add_to_path(PREPEND ${prog_env}) vcpkg_add_to_path(PREPEND ${prog_env})

View File

@@ -1,6 +1,6 @@
{ {
"name": "ffmpeg", "name": "ffmpeg",
"version": "7.0.1", "version": "7.0.2",
"port-version": 0, "port-version": 0,
"description": [ "description": [
"a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.", "a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.",

View File

@@ -84,7 +84,7 @@ pub mod io_loop;
pub const MILLI1: Duration = Duration::from_millis(1); pub const MILLI1: Duration = Duration::from_millis(1);
pub const SEC30: Duration = Duration::from_secs(30); pub const SEC30: Duration = Duration::from_secs(30);
pub const VIDEO_QUEUE_SIZE: usize = 120; 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")] #[cfg(target_os = "linux")]
pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited";
@@ -1151,6 +1151,7 @@ pub struct VideoHandler {
record: bool, record: bool,
_display: usize, // useful for debug _display: usize, // useful for debug
fail_counter: usize, fail_counter: usize,
first_frame: bool,
} }
impl VideoHandler { impl VideoHandler {
@@ -1176,6 +1177,7 @@ impl VideoHandler {
record: false, record: false,
_display, _display,
fail_counter: 0, fail_counter: 0,
first_frame: true,
} }
} }
@@ -1204,9 +1206,19 @@ impl VideoHandler {
self.fail_counter = 0; self.fail_counter = 0;
} else { } else {
if self.fail_counter < usize::MAX { 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 { if self.record {
self.recorder self.recorder
.lock() .lock()
@@ -1222,12 +1234,17 @@ impl VideoHandler {
/// Reset the decoder, change format if it is Some /// Reset the decoder, change format if it is Some
pub fn reset(&mut self, format: Option<CodecFormat>) { pub fn reset(&mut self, format: Option<CodecFormat>) {
log::info!(
"reset video handler for display #{}, format: {format:?}",
self._display
);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
self.rgb.set_align(crate::get_dst_align_rgba()); self.rgb.set_align(crate::get_dst_align_rgba());
let luid = Self::get_adapter_luid(); let luid = Self::get_adapter_luid();
let format = format.unwrap_or(self.decoder.format()); let format = format.unwrap_or(self.decoder.format());
self.decoder = Decoder::new(format, luid); self.decoder = Decoder::new(format, luid);
self.fail_counter = 0; self.fail_counter = 0;
self.first_frame = true;
} }
/// Start or stop screen record. /// Start or stop screen record.
@@ -1783,28 +1800,6 @@ impl LoginConfigHandler {
) )
} }
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
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. /// Parse the image quality option.
/// Return [`ImageQuality`] if the option is valid, otherwise return `None`. /// Return [`ImageQuality`] if the option is valid, otherwise return `None`.
/// ///
@@ -3407,3 +3402,135 @@ async fn hc_connection_(
} }
Ok(()) 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<F: FnOnce(Vec<String>, Vec<String>)>(ids: Vec<String>, 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<FramedStream> {
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<String>,
timeout: std::time::Duration,
) -> ResultType<(Vec<String>, Vec<String>)> {
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<Mutex<Option<FramedStream>>> 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<String>, offlines: Vec<String>| {
println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines);
},
)
.await;
}
}
}

View File

@@ -26,7 +26,7 @@ use crossbeam_queue::ArrayQueue;
use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::tokio::sync::mpsc::error::TryRecvError;
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
config::{PeerConfig, TransferSerde}, config::{self, PeerConfig, TransferSerde},
fs::{ fs::{
self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm,
DigestCheckResult, RemoveJobMeta, DigestCheckResult, RemoveJobMeta,
@@ -353,6 +353,7 @@ impl<T: InvokeUiSession> Remote<T> {
} else { } else {
if let Err(e) = ContextSend::make_sure_enabled() { if let Err(e) = ContextSend::make_sure_enabled() {
log::error!("failed to restart clipboard context: {}", e); 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"); log::debug!("Send system clipboard message to remote");
let msg = crate::clipboard_file::clip_2_msg(clip); let msg = crate::clipboard_file::clip_2_msg(clip);
@@ -957,22 +958,6 @@ impl<T: InvokeUiSession> Remote<T> {
true 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) { async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
if !self.peer_info.is_support_virtual_display() { if !self.peer_info.is_support_virtual_display() {
return; return;
@@ -1134,7 +1119,6 @@ impl<T: InvokeUiSession> Remote<T> {
self.first_frame = true; self.first_frame = true;
self.handler.close_success(); self.handler.close_success();
self.handler.adapt_size(); self.handler.adapt_size();
self.send_opts_after_login(peer).await;
self.send_toggle_virtual_display_msg(peer).await; self.send_toggle_virtual_display_msg(peer).await;
self.send_toggle_privacy_mode_msg(peer).await; self.send_toggle_privacy_mode_msg(peer).await;
} }
@@ -1212,18 +1196,20 @@ impl<T: InvokeUiSession> Remote<T> {
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( if self.handler.lc.read().unwrap().sync_init_clipboard.v {
&peer_version, if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg(
&peer_platform, &peer_version,
crate::clipboard::ClipboardSide::Client, &peer_platform,
) { crate::clipboard::ClipboardSide::Client,
let sender = self.sender.clone(); ) {
let permission_config = self.handler.get_permission_config(); let sender = self.sender.clone();
tokio::spawn(async move { let permission_config = self.handler.get_permission_config();
if permission_config.is_text_clipboard_required() { tokio::spawn(async move {
sender.send(Data::Message(msg_out)).ok(); if permission_config.is_text_clipboard_required() {
} sender.send(Data::Message(msg_out)).ok();
}); }
});
}
} }
// on connection established client // on connection established client
@@ -1634,7 +1620,7 @@ impl<T: InvokeUiSession> Remote<T> {
}, },
Some(message::Union::MessageBox(msgbox)) => { Some(message::Union::MessageBox(msgbox)) => {
let mut link = msgbox.link; 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(); link = v.to_string();
} else { } else {
log::warn!("Message box ignore link {} for security", &link); log::warn!("Message box ignore link {} for security", &link);
@@ -1906,7 +1892,7 @@ impl<T: InvokeUiSession> Remote<T> {
return; 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 file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v;
let stop = is_stopping_allowed && !file_transfer_enabled; let stop = is_stopping_allowed && !file_transfer_enabled;
log::debug!( log::debug!(

View File

@@ -1,9 +1,10 @@
use arboard::{ClipboardData, ClipboardFormat}; use arboard::{ClipboardData, ClipboardFormat};
use clipboard_master::{ClipboardHandler, Master, Shutdown}; use clipboard_master::{ClipboardHandler, Master, Shutdown};
use hbb_common::{log, message_proto::*, ResultType}; use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{ use std::{
sync::{mpsc::Sender, Arc, Mutex}, sync::{mpsc::Sender, Arc, Mutex},
thread::JoinHandle, thread::JoinHandle,
time::Duration,
}; };
pub const CLIPBOARD_NAME: &'static str = "clipboard"; 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. // This format is used to store the flag in the clipboard.
const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; 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! { lazy_static::lazy_static! {
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(())); static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
// cache the clipboard msg // cache the clipboard msg
@@ -23,6 +27,9 @@ lazy_static::lazy_static! {
static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = Arc::new(Mutex::new(None)); static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = 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] = &[ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
ClipboardFormat::Text, ClipboardFormat::Text,
ClipboardFormat::Html, ClipboardFormat::Html,
@@ -30,6 +37,7 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
ClipboardFormat::ImageRgba, ClipboardFormat::ImageRgba,
ClipboardFormat::ImagePng, ClipboardFormat::ImagePng,
ClipboardFormat::ImageSvg, ClipboardFormat::ImageSvg,
ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET),
ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT),
]; ];
@@ -147,14 +155,18 @@ pub fn check_clipboard(
*ctx = ClipboardContext::new().ok(); *ctx = ClipboardContext::new().ok();
} }
let ctx2 = ctx.as_mut()?; let ctx2 = ctx.as_mut()?;
let content = ctx2.get(side, force); match ctx2.get(side, force) {
if let Ok(content) = content { Ok(content) => {
if !content.is_empty() { if !content.is_empty() {
let mut msg = Message::new(); let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content); let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone()); msg.set_multi_clipboards(clipboards.clone());
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
return Some(msg); return Some(msg);
}
}
Err(e) => {
log::error!("Failed to get clipboard content. {}", e);
} }
} }
None None
@@ -259,16 +271,49 @@ impl ClipboardContext {
Ok(ClipboardContext { inner: board }) Ok(ClipboardContext { inner: board })
} }
fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType<Vec<ClipboardData>> {
// 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<Vec<ClipboardData>> { pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType<Vec<ClipboardData>> {
let _lock = ARBOARD_MTX.lock().unwrap(); 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() { if data.is_empty() {
return Ok(data); return Ok(data);
} }
if !force { if !force {
for c in data.iter() { for c in data.iter() {
if let ClipboardData::Special((_, d)) = c { if let ClipboardData::Special((s, d)) = c {
if side.is_owner(d) { if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) {
return Ok(vec![]); return Ok(vec![]);
} }
} }
@@ -276,7 +321,10 @@ impl ClipboardContext {
} }
Ok(data Ok(data
.into_iter() .into_iter()
.filter(|c| !matches!(c, ClipboardData::Special(_))) .filter(|c| match c {
ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT,
_ => true,
})
.collect()) .collect())
} }
@@ -454,12 +502,30 @@ mod proto {
} }
} }
fn special_to_proto(d: Vec<u8>, s: String) -> Clipboard {
let compressed = compress_func(&d);
let compress = compressed.len() < d.len();
let content = if compress {
compressed
} else {
s.bytes().collect::<Vec<u8>>()
};
Clipboard {
compress,
content: content.into(),
format: ClipboardFormat::Special.into(),
special_name: s,
..Default::default()
}
}
fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> { fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> {
let d = match data { let d = match data {
ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text),
ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf),
ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html),
ClipboardData::Image(a) => image_to_proto(a), ClipboardData::Image(a) => image_to_proto(a),
ClipboardData::Special((s, d)) => special_to_proto(d, s),
_ => return None, _ => return None,
}; };
Some(d) Some(d)
@@ -496,6 +562,9 @@ mod proto {
Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg(
std::str::from_utf8(&data).unwrap_or_default(), std::str::from_utf8(&data).unwrap_or_default(),
))), ))),
Ok(ClipboardFormat::Special) => {
Some(ClipboardData::Special((clipboard.special_name, data)))
}
_ => None, _ => None,
} }
} }

View File

@@ -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. // 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<RwLock<bool>> = Default::default(); static ref SERVER_RUNNING: Arc<RwLock<bool>> = Default::default();
static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--")); 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 { pub struct SimpleCallOnReturn {
@@ -137,6 +138,11 @@ pub fn is_main() -> bool {
*IS_MAIN *IS_MAIN
} }
#[inline]
pub fn is_cm() -> bool {
*IS_CM
}
// Is server logic running. // Is server logic running.
#[inline] #[inline]
pub fn is_server_running() -> bool { 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(); let response_url = latest_release_response.url().to_string();
if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { 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(()) 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()
}

View File

@@ -39,6 +39,7 @@ pub fn core_main() -> Option<Vec<String>> {
let mut _is_run_as_system = false; let mut _is_run_as_system = false;
let mut _is_quick_support = false; let mut _is_quick_support = false;
let mut _is_flutter_invoke_new_connection = false; let mut _is_flutter_invoke_new_connection = false;
let mut no_server = false;
let mut arg_exe = Default::default(); let mut arg_exe = Default::default();
for arg in std::env::args() { for arg in std::env::args() {
if i == 0 { if i == 0 {
@@ -62,6 +63,8 @@ pub fn core_main() -> Option<Vec<String>> {
_is_run_as_system = true; _is_run_as_system = true;
} else if arg == "--quick_support" { } else if arg == "--quick_support" {
_is_quick_support = true; _is_quick_support = true;
} else if arg == "--no-server" {
no_server = true;
} else { } else {
args.push(arg); args.push(arg);
} }
@@ -134,6 +137,7 @@ pub fn core_main() -> Option<Vec<String>> {
} }
} }
hbb_common::init_log(false, &log_name); hbb_common::init_log(false, &log_name);
log::info!("main start args: {:?}, env: {:?}", args, std::env::args());
// linux uni (url) go here. // linux uni (url) go here.
#[cfg(all(target_os = "linux", feature = "flutter"))] #[cfg(all(target_os = "linux", feature = "flutter"))]
@@ -161,9 +165,8 @@ pub fn core_main() -> Option<Vec<String>> {
#[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
init_plugins(&args); init_plugins(&args);
log::info!("main start args:{:?}", args);
if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { 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 { } else {
#[cfg(windows)] #[cfg(windows)]
{ {
@@ -279,11 +282,11 @@ pub fn core_main() -> Option<Vec<String>> {
crate::privacy_mode::restore_reg_connectivity(true); crate::privacy_mode::restore_reg_connectivity(true);
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
{ {
crate::start_server(true); crate::start_server(true, false);
} }
#[cfg(target_os = "macos")] #[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(); crate::tray::start_tray();
// prevent server exit when encountering errors from tray // prevent server exit when encountering errors from tray
hbb_common::allow_err!(handler.join()); hbb_common::allow_err!(handler.join());
@@ -473,8 +476,18 @@ pub fn core_main() -> Option<Vec<String>> {
crate::ui_interface::start_option_status_sync(); crate::ui_interface::start_option_status_sync();
} else if args[0] == "--cm-no-ui" { } else if args[0] == "--cm-no-ui" {
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "windows")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::flutter::connection_manager::start_cm_no_ui(); {
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; return None;
} else { } else {
#[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(all(feature = "flutter", feature = "plugin_framework"))]

View File

@@ -802,13 +802,13 @@ impl InvokeUiSession for FlutterHandler {
fn set_peer_info(&self, pi: &PeerInfo) { fn set_peer_info(&self, pi: &PeerInfo) {
let displays = Self::make_displays_msg(&pi.displays); 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() { 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 // compatible with 1.1.9
if get_version_number(&pi.version) < get_version_number("1.2.0") { 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 features = serde_json::ser::to_string(&features).unwrap_or("".to_owned());
let resolutions = serialize_resolutions(&pi.resolutions.resolutions); let resolutions = serialize_resolutions(&pi.resolutions.resolutions);
@@ -2057,18 +2057,18 @@ pub mod sessions {
pub(super) mod async_tasks { pub(super) mod async_tasks {
use hbb_common::{ use hbb_common::{
bail, bail,
tokio::{ tokio::{self, select},
self, select,
sync::mpsc::{unbounded_channel, UnboundedSender},
},
ResultType, ResultType,
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{Arc, Mutex}, sync::{
mpsc::{sync_channel, SyncSender},
Arc, Mutex,
},
}; };
type TxQueryOnlines = UnboundedSender<Vec<String>>; type TxQueryOnlines = SyncSender<Vec<String>>;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref TX_QUERY_ONLINES: Arc<Mutex<Option<TxQueryOnlines>>> = Default::default(); static ref TX_QUERY_ONLINES: Arc<Mutex<Option<TxQueryOnlines>>> = Default::default();
} }
@@ -2085,21 +2085,18 @@ pub(super) mod async_tasks {
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn start_flutter_async_runner_() { async fn start_flutter_async_runner_() {
let (tx_onlines, mut rx_onlines) = unbounded_channel::<Vec<String>>(); // Only one task is allowed to run at the same time.
let (tx_onlines, rx_onlines) = sync_channel::<Vec<String>>(1);
TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines); TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines);
loop { loop {
select! { match rx_onlines.recv() {
ids = rx_onlines.recv() => { Ok(ids) => {
match ids { crate::client::peer_online::query_online_states(ids, handle_query_onlines).await
Some(_ids) => { }
#[cfg(not(any(target_os = "ios")))] _ => {
crate::rendezvous_mediator::query_online_states(_ids, handle_query_onlines).await // unreachable!
} break;
None => {
break;
}
}
} }
} }
} }
@@ -2107,7 +2104,8 @@ pub(super) mod async_tasks {
pub fn query_onlines(ids: Vec<String>) -> ResultType<()> { pub fn query_onlines(ids: Vec<String>) -> ResultType<()> {
if let Some(tx) = TX_QUERY_ONLINES.lock().unwrap().as_ref() { 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 { } else {
bail!("No tx_query_onlines"); bail!("No tx_query_onlines");
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
client::file_trait::FileManager, client::file_trait::FileManager,
common::{is_keyboard_mode_supported, make_fd_to_json}, common::make_fd_to_json,
flutter::{ flutter::{
self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, 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::{ use hbb_common::{
config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
fs, lazy_static, log, fs, lazy_static, log,
message_proto::KeyboardMode,
rendezvous_proto::ConnType, rendezvous_proto::ConnType,
ResultType, ResultType,
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
str::FromStr,
sync::{ sync::{
atomic::{AtomicI32, Ordering}, atomic::{AtomicI32, Ordering},
Arc, Arc,
@@ -63,7 +61,6 @@ fn initialize(app_dir: &str, custom_client_config: &str) {
scrap::mediacodec::check_mediacodec(); scrap::mediacodec::check_mediacodec();
crate::common::test_rendezvous_server(); crate::common::test_rendezvous_server();
crate::common::test_nat_type(); crate::common::test_nat_type();
crate::common::check_software_update();
} }
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
{ {
@@ -447,15 +444,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32
pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn<bool> { pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn<bool> {
if let Some(session) = sessions::get_session_by_session_id(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { SyncReturn(session.is_keyboard_mode_supported(mode))
SyncReturn(is_keyboard_mode_supported(
&mode,
session.get_peer_version(),
&session.peer_platform(),
))
} else {
SyncReturn(false)
}
} else { } else {
SyncReturn(false) SyncReturn(false)
} }
@@ -490,6 +479,25 @@ pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Ve
} }
pub fn session_handle_flutter_key_event( 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, session_id: SessionID,
name: String, name: String,
platform_code: i32, platform_code: i32,
@@ -499,7 +507,7 @@ pub fn session_handle_flutter_key_event(
) { ) {
if let Some(session) = sessions::get_session_by_session_id(&session_id) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode(); let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_key_event( session.handle_flutter_raw_key_event(
&keyboard_mode, &keyboard_mode,
&name, &name,
platform_code, platform_code,
@@ -1367,11 +1375,10 @@ pub fn main_get_last_remote_id() -> String {
LocalConfig::get_remote_id() 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" { if get_local_option("enable-check-update".to_string()) != "N" {
crate::common::check_software_update(); crate::common::check_software_update();
} }
crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone()
} }
pub fn main_get_home_dir() -> String { pub fn main_get_home_dir() -> String {
@@ -2273,6 +2280,10 @@ pub fn main_clear_trusted_devices() {
clear_trusted_devices() clear_trusted_devices()
} }
pub fn main_max_encrypt_len() -> SyncReturn<usize> {
SyncReturn(max_encrypt_len())
}
pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) { 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) { if let Some(session) = sessions::get_session_by_session_id(&session_id) {
session.request_init_msgs(display); session.request_init_msgs(display);

View File

@@ -403,7 +403,8 @@ async fn handle(data: Data, stream: &mut Connection) {
{ {
hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0) hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0)
.await; .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")] #[cfg(target_os = "macos")]
{ {
@@ -928,16 +929,23 @@ pub fn set_permanent_password(v: String) -> ResultType<()> {
pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> { pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> {
let v = v.trim().to_owned(); let v = v.trim().to_owned();
let min_len = 4; let min_len = 4;
if !v.is_empty() && v.len() < min_len { let max_len = crate::ui_interface::max_encrypt_len();
let err = if translate { let len = v.chars().count();
crate::lang::translate( if !v.is_empty() {
"Requires at least {".to_string() + &format!("{min_len}") + "} characters", if len < min_len {
) let err = if translate {
} else { crate::lang::translate(
// Sometimes, translated can't show normally in command line "Requires at least {".to_string() + &format!("{min_len}") + "} characters",
format!("Requires at least {} characters", min_len) )
}; } else {
bail!(err); // 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); Config::set_unlock_pin(&v);
set_config("unlock-pin", v) set_config("unlock-pin", v)

View File

@@ -34,6 +34,7 @@ const OS_LOWER_ANDROID: &str = "android";
#[cfg(any(target_os = "windows", target_os = "macos"))] #[cfg(any(target_os = "windows", target_os = "macos"))]
static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false); static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false);
@@ -71,6 +72,7 @@ pub mod client {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn change_grab_status(state: GrabState, keyboard_mode: &str) { pub fn change_grab_status(state: GrabState, keyboard_mode: &str) {
#[cfg(feature = "flutter")]
if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { if !IS_RDEV_ENABLED.load(Ordering::SeqCst) {
return; return;
} }

View File

@@ -1,4 +1,3 @@
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::config::Config; use hbb_common::config::Config;
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
@@ -22,7 +21,7 @@ use std::{
type Message = RendezvousMessage; type Message = RendezvousMessage;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(target_os = "ios"))]
pub(super) fn start_listening() -> ResultType<()> { pub(super) fn start_listening() -> ResultType<()> {
let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port()));
let socket = std::net::UdpSocket::bind(addr)?; let socket = std::net::UdpSocket::bind(addr)?;
@@ -40,13 +39,22 @@ pub(super) fn start_listening() -> ResultType<()> {
&Config::get_option("enable-lan-discovery"), &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) { if let Some(self_addr) = get_ipaddr_by_peer(&addr) {
let mut msg_out = Message::new(); 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 { let peer = PeerDiscovery {
cmd: "pong".to_owned(), cmd: "pong".to_owned(),
mac: get_mac(&self_addr), mac: get_mac(&self_addr),
id: Config::get_id(), id,
hostname: whoami::hostname(), hostname,
username: crate::platform::get_active_username(), username: crate::platform::get_active_username(),
platform: whoami::platform().to_string(), platform: whoami::platform().to_string(),
..Default::default() ..Default::default()
@@ -100,17 +108,17 @@ fn get_broadcast_port() -> u16 {
} }
fn get_mac(_ip: &IpAddr) -> String { 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) { if let Ok(mac) = get_mac_by_ip(_ip) {
mac.to_string() mac.to_string()
} else { } else {
"".to_owned() "".to_owned()
} }
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(target_os = "ios")]
"".to_owned() "".to_owned()
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(target_os = "ios"))]
fn get_mac_by_ip(ip: &IpAddr) -> ResultType<String> { fn get_mac_by_ip(ip: &IpAddr) -> ResultType<String> {
for interface in default_net::get_interfaces() { for interface in default_net::get_interfaces() {
match ip { match ip {
@@ -153,6 +161,10 @@ fn get_ipaddr_by_peer<A: ToSocketAddrs>(peer: A) -> Option<IpAddr> {
fn create_broadcast_sockets() -> Vec<UdpSocket> { fn create_broadcast_sockets() -> Vec<UdpSocket> {
let mut ipv4s = Vec::new(); 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 interface in default_net::get_interfaces() {
for ipv4 in &interface.ipv4 { for ipv4 in &interface.ipv4 {
ipv4s.push(ipv4.addr.clone()); ipv4s.push(ipv4.addr.clone());
@@ -178,8 +190,20 @@ fn send_query() -> ResultType<Vec<UdpSocket>> {
} }
let mut msg_out = Message::new(); 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 { let peer = PeerDiscovery {
cmd: "ping".to_owned(), cmd: "ping".to_owned(),
id,
..Default::default() ..Default::default()
}; };
msg_out.set_peer_discovery(peer); msg_out.set_peer_discovery(peer);

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""), ("Parent directory", ""),
("Resume", ""), ("Resume", ""),
("Invalid file name", ""), ("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""), ("Parent directory", ""),
("Resume", ""), ("Resume", ""),
("Invalid file name", ""), ("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -1,19 +1,19 @@
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> = pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[ [
("Status", "Статус"), ("Status", "Положение"),
("Your Desktop", "Твоят Работен Плот"), ("Your Desktop", "Вашата работна среда"),
("desk_tip", "Вашият работен плот може да бъде достъпен с този идентификационен код и парола."), ("desk_tip", "Вашата работна среда не може да бъде достъпена с този потребителски код и парола."),
("Password", "Парола"), ("Password", "Парола"),
("Ready", "Готово"), ("Ready", "Готово"),
("Established", "Установен"), ("Established", "Установен"),
("connecting_status", "Свързване с RustDesk мрежата..."), ("connecting_status", "Свързване с RustDesk мрежата..."),
("Enable service", "Пусни услуга"), ("Enable service", "Разреши услуга"),
("Start service", "Стартирай услуга"), ("Start service", "Стартирай услуга"),
("Service is running", "Услугата работи"), ("Service is running", "Услугата работи"),
("Service is not running", "Услугата не работи"), ("Service is not running", "Услугата не работи"),
("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"), ("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"),
("Control Remote Desktop", "Контролирайте отдалечения работен плот"), ("Control Remote Desktop", "Отдалечено управление на работна среда"),
("Transfer file", "Прехвърляне на файл"), ("Transfer file", "Прехвърляне на файл"),
("Connect", "Свързване"), ("Connect", "Свързване"),
("Recent sessions", "Последни сесии"), ("Recent sessions", "Последни сесии"),
@@ -23,27 +23,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remove", "Премахване"), ("Remove", "Премахване"),
("Refresh random password", "Опресняване на произволна парола"), ("Refresh random password", "Опресняване на произволна парола"),
("Set your own password", "Задайте собствена парола"), ("Set your own password", "Задайте собствена парола"),
("Enable keyboard/mouse", "Разрешение на клавиатура/мишка"), ("Enable keyboard/mouse", "Позволяване на клавиатура/мишка"),
("Enable clipboard", "Разрешение на клипборда"), ("Enable clipboard", "Позволяване достъп до клипборда"),
("Enable file transfer", "Разрешение прехвърлянето на файлове"), ("Enable file transfer", "Позволяване прехвърляне на файлове"),
("Enable TCP tunneling", "Разрешение за TCP тунел"), ("Enable TCP tunneling", "Позволяване на TCP тунели"),
("IP Whitelisting", "IP беял списък"), ("IP Whitelisting", "Определяне на позволени IP по списък"),
("ID/Relay Server", "ID/Релейн сървър"), ("ID/Relay Server", "ID/Препредаващ сървър"),
("Import server config", "Експортиране конфигурацията на сървъра"), ("Import server config", "Внасяне сървър настройки за "),
("Export Server Config", "Експортиране на конфигурация на сървъра"), ("Export Server Config", "Изнасяне настройки на сървър"),
("Import server configuration successfully", "Импортирането конфигурацията на сървъра успешно"), ("Import server configuration successfully", "Успешно внасяне на сървърни настройки"),
("Export server configuration successfully", "Експортирането конфигурацията на сървъра успешно"), ("Export server configuration successfully", "Успешно изнасяне на сървърни настройки"),
("Invalid server configuration", "Невалидна конфигурация на сървъра"), ("Invalid server configuration", "Недопустими сървърни настройки"),
("Clipboard is empty", "Клипбордът е празен"), ("Clipboard is empty", "Клипбордът е празен"),
("Stop service", "Спрете услугата"), ("Stop service", "Спираане на услуга"),
("Change ID", "Промяна на ID"), ("Change ID", "Промяна определител (ID)"),
("Your new ID", "Вашето ново ID"), ("Your new ID", "Вашият нов определител (ID)"),
("length %min% to %max%", "дължина %min% до %max%"), ("length %min% to %max%", "дължина %min% до %max%"),
("starts with a letter", "започва с буква"), ("starts with a letter", "започва с буква"),
("allowed characters", "разрешени знаци"), ("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", "Уебсайт"), ("Website", "Уебсайт"),
("About", "Относно програмата"), ("About", "Относно"),
("Slogan_tip", "Направено от сърце в този хаотичен свят!"), ("Slogan_tip", "Направено от сърце в този хаотичен свят!"),
("Privacy Statement", "Декларация за поверителност"), ("Privacy Statement", "Декларация за поверителност"),
("Mute", "Без звук"), ("Mute", "Без звук"),
@@ -53,23 +53,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input", "Аудио вход"), ("Audio Input", "Аудио вход"),
("Enhancements", "Подобрения"), ("Enhancements", "Подобрения"),
("Hardware Codec", "Хардуерен кодек"), ("Hardware Codec", "Хардуерен кодек"),
("Adaptive bitrate", "Адаптивен битрейт"), ("Adaptive bitrate", "Приспособяваще се скорост на предаване наданни"),
("ID Server", "ID сървър"), ("ID Server", "ID сървър"),
("Relay Server", "Релейн сървър"), ("Relay Server", "Препращащ сървър"),
("API Server", "API сървър"), ("API Server", "API сървър"),
("invalid_http", "трябва да започва с http:// или https://"), ("invalid_http", "трябва да започва с http:// или https://"),
("Invalid IP", "Невалиден IP"), ("Invalid IP", "Недопустим IP"),
("Invalid format", "Невалиден формат"), ("Invalid format", "Недопустим формат"),
("server_not_support", "Все още не се поддържа от сървъра"), ("server_not_support", "Все още не се поддържа от сървъра"),
("Not available", "Не е наличен"), ("Not available", "Не е наличен"),
("Too frequent", "Твърде често"), ("Too frequent", "Твърде често"),
("Cancel", "Отказ"), ("Cancel", "Отказ"),
("Skip", "Пропускане"), ("Skip", "Пропускане"),
("Close", "Затвори"), ("Close", "Затваряне"),
("Retry", "Опитайте отново"), ("Retry", "Преповтори"),
("OK", "Добре"), ("OK", "Добре"),
("Password Required", "Изисква се парола"), ("Password Required", "Изисква се парола"),
("Please enter your password", "Моля въведете паролата си"), ("Please enter your password", "Моля въведете парола"),
("Remember password", "Запомни паролата"), ("Remember password", "Запомни паролата"),
("Wrong Password", "Грешна парола"), ("Wrong Password", "Грешна парола"),
("Do you want to enter again?", "Искате ли да въведете отново?"), ("Do you want to enter again?", "Искате ли да въведете отново?"),
@@ -99,73 +99,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Properties", "Свойства"), ("Properties", "Свойства"),
("Multi Select", "Множествен избор"), ("Multi Select", "Множествен избор"),
("Select All", "Избери всички"), ("Select All", "Избери всички"),
("Unselect All", "Деселектирай всички"), ("Unselect All", "Избери никой"),
("Empty Directory", "Празна директория"), ("Empty Directory", "Празна папка"),
("Not an empty directory", "Не е празна директория"), ("Not an empty directory", "Не е празна папка"),
("Are you sure you want to delete this file?", "Сигурни ли сте, че искате да изтриете този файл?"), ("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 this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна папка?"),
("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази директория?"), ("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази папка?"),
("Do this for all conflicts", "Направете това за всички конфликти"), ("Do this for all conflicts", "Разреши така всички конфликти"),
("This is irreversible!", ""), ("This is irreversible!", "Това е необратимо!"),
("Deleting", "Изтриване"), ("Deleting", "Изтриване"),
("files", "файлове"), ("files", "файлове"),
("Waiting", ""), ("Waiting", "Изчакване"),
("Finished", "Готово"), ("Finished", "Завършено"),
("Speed", "Скорост"), ("Speed", "Скорост"),
("Custom Image Quality", "Персонализирано качество на изображението"), ("Custom Image Quality", "Качество на изображението по свой избор"),
("Privacy mode", "Режим на поверителност"), ("Privacy mode", "Режим на поверителност"),
("Block user input", "Блокиране на потребителско въвеждане"), ("Block user input", "Забрана за потребителски вход"),
("Unblock user input", "Отблокиране на потребителско въвеждане"), ("Unblock user input", "Разрешаване на потребителски въвеждане"),
("Adjust Window", "Регулирай прозореца"), ("Adjust Window", "Нагласи прозореца"),
("Original", "Оригинално"), ("Original", "Оригинално"),
("Shrink", "Свиване"), ("Shrink", "Свиване"),
("Stretch", "Разтегнат"), ("Stretch", "Разтегнат"),
("Scrollbar", "Плъзгач"), ("Scrollbar", "Плъзгач"),
("ScrollAuto", "Автоматичен плъзгач"), ("ScrollAuto", "Автоматичено приплъзване"),
("Good image quality", "Добро качество на изображението"), ("Good image quality", "Добро качество на изображението"),
("Balanced", "Балансиран"), ("Balanced", "Уравновесен"),
("Optimize reaction time", "Оптимизирайте времето за реакция"), ("Optimize reaction time", "С оглед времето на реакция"),
("Custom", "Персонализиран"), ("Custom", "По собствено желание"),
("Show remote cursor", "Показване на дистанционния курсор"), ("Show remote cursor", "Показвай отдалечения курсор"),
("Show quality monitor", "Показване на прозорец за качество"), ("Show quality monitor", "Показвай прозорец за качество"),
("Disable clipboard", "Деактивиране на клипборда"), ("Disable clipboard", "Забрана за достъп до клипборд"),
("Lock after session end", "Заключване след края на сесията"), ("Lock after session end", "Заключване след край на ползване"),
("Insert", "Поставяне"), ("Insert", "Поставяне"),
("Insert Lock", "Заявка за заключване"), ("Insert Lock", "Заявка за заключване"),
("Refresh", "Обнови"), ("Refresh", "Обновяване"),
("ID does not exist", "ID-то не съществува"), ("ID does not exist", "Несъществуващ определител (ID)"),
("Failed to connect to rendezvous server", "Неуспешно свързване със сървъра за рандеву"), ("Failed to connect to rendezvous server", "Неуспешно свързване към сървъра за среща (rendezvous)"),
("Please try later", "Моля опитайте по-късно"), ("Please try later", "Моля опитайте по-късно"),
("Remote desktop is offline", "Отдалеченият работен плот е офлайн"), ("Remote desktop is offline", "Отдалечената работна среда не е налична"),
("Key mismatch", "Ключово несъответствие"), ("Key mismatch", "Ключово несъответствие"),
("Timeout", ""), ("Timeout", "Изтичане на времето"),
("Failed to connect to relay server", ""), ("Failed to connect to relay server", "Провал при свързване към препредаващ сървър"),
("Failed to connect via rendezvous server", ""), ("Failed to connect via rendezvous server", "Провал при свързване към сървър за срещи (rendezvous)"),
("Failed to connect via relay server", ""), ("Failed to connect via relay server", "Провал при свързване чрез препредаващ сървър"),
("Failed to make direct connection to remote desktop", ""), ("Failed to make direct connection to remote desktop", "Провал при установяване на пряка връзка с отдалечена работна среда"),
("Set Password", "Задайте парола"), ("Set Password", "Задаване на парола"),
("OS Password", "Парола на Операционната система"), ("OS Password", "Парола на Операционната система"),
("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно като отдалечена достъп. За да заобиколите UAC, моля, щракнете върху бутона по-долу, за да инсталирате RustDesk в системата."), ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно за отдалечена достъп. За да заобиколите UAC, моля, натиснете копчето по-долу, за да поставите RustDesk като системна услуга."),
("Click to upgrade", "Кликнете, за да надстроите"), ("Click to upgrade", "Натиснете, за да надстроите"),
("Click to download", "Кликнете, за да изтеглите"), ("Click to download", "Натиснете, за да изтеглите"),
("Click to update", "Кликнете, за да актуализирате"), ("Click to update", "Натиснете, за да обновите"),
("Configure", "Конфигуриране"), ("Configure", "Настройване"),
("config_acc", "За да управлявате вашия работен плот дистанционно, трябва да предоставите на RustDesk разрешения \"Достъпност\"."), ("config_acc", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Достъпност\"."),
("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."), ("config_screen", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Запис на екрана\"."),
("Installing ...", "Инсталиране..."), ("Installing ...", "Поставяне..."),
("Install", "Инсталирай"), ("Install", "Постави"),
("Installation", "Инсталация"), ("Installation", "Поставяне"),
("Installation Path", "Инсталационен път"), ("Installation Path", "Път към място за поставяне"),
("Create start menu shortcuts", "Създайте преки пътища в менюто 'Старт'."), ("Create start menu shortcuts", "Бърз достъп от меню 'Старт'."),
("Create desktop icon", "Създайте икона на работния плот"), ("Create desktop icon", "Създайте икона на работния плот"),
("agreement_tip", "Стартирайки инсталацията, вие приемате лицензионното споразумение."), ("agreement_tip", "Започвайки поставянето, вие приемате лицензионното споразумение."),
("Accept and Install", "Приемете и инсталирайте"), ("Accept and Install", "Приемете и поставяте"),
("End-user license agreement", ""), ("End-user license agreement", "Споразумение с потребителя"),
("Generating ...", "Генериране..."), ("Generating ...", "Пораждане..."),
("Your installation is lower version.", "Вашата инсталация е по-ниска версия."), ("Your installation is lower version.", "Вашата инсталация е по-ниска версия."),
("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"), ("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"),
("Listening ...", "Слушане..."), ("Listening ...", "Слушане..."),
("Remote Host", "Отдалечен хост"), ("Remote Host", "Отдалечен сървър"),
("Remote Port", "Отдалечен порт"), ("Remote Port", "Отдалечен порт"),
("Action", "Действие"), ("Action", "Действие"),
("Add", "Добави"), ("Add", "Добави"),
@@ -173,154 +173,154 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Local Address", "Локален адрес"), ("Local Address", "Локален адрес"),
("Change Local Port", "Промяна на локалният порт"), ("Change Local Port", "Промяна на локалният порт"),
("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"), ("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"),
("Too short, at least 6 characters.", ""), ("Too short, at least 6 characters.", "Прекалено кратко, поне 6 знака"),
("The confirmation is not identical.", ""), ("The confirmation is not identical.", "Потвърждението не съвпада"),
("Permissions", "Разрешения"), ("Permissions", "Разрешения"),
("Accept", "Приеми"), ("Accept", "Приеми"),
("Dismiss", "Отхвърляне"), ("Dismiss", "Отхвърляне"),
("Disconnect", "Прекъснете връзката"), ("Disconnect", "Прекъсване"),
("Enable file copy and paste", ""), ("Enable file copy and paste", "Разрешаване прехвърляне на файлове"),
("Connected", "Свързан"), ("Connected", "Свързан"),
("Direct and encrypted connection", "Директна и криптирана връзка"), ("Direct and encrypted connection", "Пряка защитена връзка"),
("Relayed and encrypted connection", "Препредадена и криптирана връзка"), ("Relayed and encrypted connection", "Препредадена защитена връзка"),
("Direct and unencrypted connection", "Директна и некриптирана връзка"), ("Direct and unencrypted connection", "Пряка незащитена връзка"),
("Relayed and unencrypted connection", "Препредадена и некриптирана връзка"), ("Relayed and unencrypted connection", "Препредадена незащитена връзка"),
("Enter Remote ID", "Въведете дистанционно ID"), ("Enter Remote ID", "Въведете отдалеченото ID"),
("Enter your password", "Въведете паролата си"), ("Enter your password", "Въведете парола"),
("Logging in...", ""), ("Logging in...", "Вписване..."),
("Enable RDP session sharing", "Активирайте споделянето на RDP сесия"), ("Enable RDP session sharing", "Позволяване споделянето на RDP сесия"),
("Auto Login", "Автоматично вписване (Валидно само ако зададете \"Заключване след края на сесията\")"), ("Auto Login", "Автоматично вписване (Валидно само ако зададете \"Заключване след края на сесията\")"),
("Enable direct IP access", "Разрешете директен IP достъп"), ("Enable direct IP access", "Разрешаване пряк IP достъп"),
("Rename", "Преименуване"), ("Rename", "Преименуване"),
("Space", "Пространство"), ("Space", "Пространство"),
("Create desktop shortcut", "Създайте пряк път на работния плот"), ("Create desktop shortcut", "Създайте пряк път на работния плот"),
("Change Path", "Промяна на пътя"), ("Change Path", "Промяна на пътя"),
("Create Folder", "Създай папка"), ("Create Folder", "Създай папка"),
("Please enter the folder name", "Моля, въведете името на папката"), ("Please enter the folder name", "Моля, въведете име на папката"),
("Fix it", "Оправи го"), ("Fix it", "Оправи го"),
("Warning", "Внимание"), ("Warning", "Внимание"),
("Login screen using Wayland is not supported", "Екранът за влизане с помощта на Wayland не се поддържа"), ("Login screen using Wayland is not supported", "Екран за влизане чрез Wayland не се поддържа"),
("Reboot required", "Изисква се рестартиране"), ("Reboot required", "Нужно е презареждане на ОС"),
("Unsupported display server", "Неподдържан сървър за дисплея"), ("Unsupported display server", "Неподдържан екранен сървър"),
("x11 expected", ""), ("x11 expected", "Очаква се x11"),
("Port", "Порт"), ("Port", "Порт"),
("Settings", "Настройки"), ("Settings", "Настройки"),
("Username", "Потребителско име"), ("Username", "Потребителско име"),
("Invalid port", "Невалиден порт"), ("Invalid port", "Недопустим порт"),
("Closed manually by the peer", "Затворено ръчно от партньора"), ("Closed manually by the peer", "Затворено ръчно от другата страна"),
("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"), ("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"),
("Run without install", "Стартирайте без инсталиране"), ("Run without install", "Стартирайте без инсталиране"),
("Connect via relay", "Свържете чрез реле"), ("Connect via relay", "Свързване чрез препращане"),
("Always connect via relay", "Винаги свързвайте чрез реле"), ("Always connect via relay", "Винаги чрез препращане"),
("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"), ("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"),
("Login", "Влизане"), ("Login", "Влизане"),
("Verify", "Потвърди"), ("Verify", "Потвърди"),
("Remember me", "Запомни ме"), ("Remember me", "Запомни ме"),
("Trust this device", "Доверете се на това устройство"), ("Trust this device", "Доверяване на това устройство"),
("Verification code", "Код за потвърждение"), ("Verification code", "Код за потвърждение"),
("verification_tip", "На регистрирания имейл адрес е изпратен код за потвърждение, въведете кода за потвърждение, за да продължите да влизате."), ("verification_tip", "На посочения имейл е изпратен код за потвърждение. Моля въведете го, за да продължите с влизането."),
("Logout", "Излез от профила си"), ("Logout", "Отписване (Изход)"),
("Tags", "Етикети"), ("Tags", "Белези"),
("Search ID", "Търсене на ID"), ("Search ID", "Търси ID"),
("whitelist_sep", "Разделени със запетая, точка и запетая, интервали или нов ред"), ("whitelist_sep", "Разделени със запетая, точка и запетая, празни символи или нов ред"),
("Add ID", "Добави ID"), ("Add ID", "Добави ID"),
("Add Tag", "Добави етикет"), ("Add Tag", "Добави етикет"),
("Unselect all tags", "Премахнете избора на всички етикети"), ("Unselect all tags", "Премахнете избора на всички белези (tags)"),
("Network error", "Мрежова грешка"), ("Network error", "Мрежова грешка"),
("Username missed", "Пропуснато потребителско име"), ("Username missed", "Липсващо потребителско име"),
("Password missed", "Пропусната парола"), ("Password missed", "Липсваща парола"),
("Wrong credentials", "Wrong username or password"), ("Wrong credentials", "Грешни пълномощия"),
("The verification code is incorrect or has expired", ""), ("The verification code is incorrect or has expired", "Кодът за проверка е неправилен или с изтекла давност."),
("Edit Tag", "Edit tag"), ("Edit Tag", "Промени белег"),
("Forget Password", "Забравена парола"), ("Forget Password", "Забравена парола"),
("Favorites", ""), ("Favorites", "Любими"),
("Add to Favorites", "Добави към любими"), ("Add to Favorites", "Добави към любими"),
("Remove from Favorites", "Премахване от любими"), ("Remove from Favorites", "Премахване от любими"),
("Empty", "Празно"), ("Empty", "Празно"),
("Invalid folder name", ""), ("Invalid folder name", "Непозволено име на папка"),
("Socks5 Proxy", "Socks5 прокси"), ("Socks5 Proxy", "Socks5 посредник"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) посредник"),
("Discovered", ""), ("Discovered", "Открит"),
("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."), ("install_daemon_tip", "За зареждане при стартиране на ОС следва да поставите RustDesk като системна услуга."),
("Remote ID", "Дистанционно ID"), ("Remote ID", "Отдалечено ID"),
("Paste", "Постави"), ("Paste", "Постави"),
("Paste here?", "Постави тук?"), ("Paste here?", "Постави тук?"),
("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"), ("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"),
("Download new version", ""), ("Download new version", "Изтегляне на нова версия"),
("Touch mode", "Режим тъч (сензорен)"), ("Touch mode", "Режим сензорен (touch)"),
("Mouse mode", "Режим мишка"), ("Mouse mode", "Режим мишка"),
("One-Finger Tap", "Докосване с един пръст"), ("One-Finger Tap", "Допир с един пръст"),
("Left Mouse", "Ляв бутон на мишката"), ("Left Mouse", "Ляв бутон на мишката"),
("One-Long Tap", "Едно дълго докосване"), ("One-Long Tap", "Дълъг допир"),
("Two-Finger Tap", "Докосване с два пръста"), ("Two-Finger Tap", "Допир с два пръста"),
("Right Mouse", "Десен бутон на мишката"), ("Right Mouse", "Десен бутон на мишката"),
("One-Finger Move", "Преместване с един пръст"), ("One-Finger Move", "Преместване с един пръст"),
("Double Tap & Move", "Докоснете два пъти и преместете"), ("Double Tap & Move", "Двоен допир и преместване"),
("Mouse Drag", "Плъзгане с мишката"), ("Mouse Drag", "Провличане с мишката"),
("Three-Finger vertically", "Три пръста вертикално"), ("Three-Finger vertically", "Три пръста вертикално"),
("Mouse Wheel", "Колело на мишката"), ("Mouse Wheel", "Колело на мишката"),
("Two-Finger Move", "Движение с два пръста"), ("Two-Finger Move", "Движение с два пръста"),
("Canvas Move", "Преместване на платното"), ("Canvas Move", "Преместване на платното"),
("Pinch to Zoom", "Щипнете, за да увеличите"), ("Pinch to Zoom", "Щипнете, за да увеличите"),
("Canvas Zoom", "Увеличение на платното"), ("Canvas Zoom", "Увеличение на платното"),
("Reset canvas", ""), ("Reset canvas", "Нулиране на платното"),
("No permission of file transfer", ""), ("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"),
("Note", ""), ("Note", "Бележка"),
("Connection", ""), ("Connection", "Връзка"),
("Share Screen", "Сподели екран"), ("Share Screen", "Сподели екран"),
("Chat", "Чат"), ("Chat", "Говор"),
("Total", "Обшо"), ("Total", "Общо"),
("items", "елементи"), ("items", "неща"),
("Selected", "Избрано"), ("Selected", "Избрано"),
("Screen Capture", "Заснемане на екрана"), ("Screen Capture", "Снемане на екрана"),
("Input Control", "Контрол на въвеждане"), ("Input Control", "Управление на вход"),
("Audio Capture", "Аудио записване"), ("Audio Capture", "Аудиозапис"),
("File Connection", "Файлова връзка"), ("File Connection", "Файлова връзка"),
("Screen Connection", "Свързване на екрана"), ("Screen Connection", "Екранна връзка"),
("Do you accept?", "Приемате ли?"), ("Do you accept?", "Приемате ли?"),
("Open System Setting", "Отворете системната настройка"), ("Open System Setting", "Отворете системните настройки"),
("How to get Android input permission?", ""), ("How to get Android input permission?", "Как да получим право за въвеждане под Андрид?"),
("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или докосване, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или допир, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."),
("android_input_permission_tip2", "Моля, отидете на следващата страница с системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."), ("android_input_permission_tip2", "Моля, отидете на следващата страница с системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."),
("android_new_connection_tip", "Получена е нова заявка за контрол, която иска да контролира вашето текущо устройство."), ("android_new_connection_tip", "Получена е нова заявка за отдалечено управление на вашето текущо устройство."),
("android_service_will_start_tip", "Включването на \"Заснемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), ("android_service_will_start_tip", "Включването на \"Снемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."),
("android_stop_service_tip", "Затварянето на услугата автоматично ще затвори всички установени връзки."), ("android_stop_service_tip", "Затварянето на услугата автоматично ще затвори всички установени връзки."),
("android_version_audio_tip", "Текущата версия на Android не поддържа аудио заснемане, моля, актуализирайте устройството с Android 10 или по-нова версия."), ("android_version_audio_tip", "Текущата версия на Android не поддържа аудиозапис. Моля, актуализирайте устройството с Android 10 или по-нов."),
("android_start_service_tip", "Докоснете [Start service] или активирайте разрешение [Screen Capture], за да стартирате услугата за споделяне на екрана."), ("android_start_service_tip", "Докоснете [Start service] или позволете [Screen Capture], за да започне услугата по споделяне на екрана."),
("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, докато не се свържете отново."), ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, а ще изискват да се свържете отново."),
("Account", "Акаунт"), ("Account", "Сметка"),
("Overwrite", "Презаписване"), ("Overwrite", "Презаписване"),
("This file exists, skip or overwrite this file?", ""), ("This file exists, skip or overwrite this file?", "Този файл съществува вече. Пропускане или презаписване?"),
("Quit", "Излез"), ("Quit", "Изход"),
("Help", "Помощ"), ("Help", "Помощ"),
("Failed", "Неуспешно"), ("Failed", "Неуспешно"),
("Succeeded", "Успешно"), ("Succeeded", "Успешно"),
("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, излезте"), ("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, изход"),
("Unsupported", "Не се поддържа"), ("Unsupported", "Неподдържан"),
("Peer denied", ""), ("Peer denied", "Отказ от другата страна"),
("Please install plugins", ""), ("Please install plugins", "Моля поставете приставки"),
("Peer exit", ""), ("Peer exit", "Изход от другата страна"),
("Failed to turn off", ""), ("Failed to turn off", "Провал при опит за изключване"),
("Turned off", ""), ("Turned off", "Изкключен"),
("Language", "Език"), ("Language", "Език"),
("Keep RustDesk background service", ""), ("Keep RustDesk background service", "Запази работеща фонова услуга с RustDesk"),
("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"), ("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"),
("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"), ("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"),
("Start on boot", "Стартирайте при зареждане"), ("Start on boot", "Стартирайте при зареждане"),
("Start the screen sharing service on boot, requires special permissions", ""), ("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", ""), ("Connection not allowed", "Връзката непозволена"),
("Legacy mode", ""), ("Legacy mode", "По остарял начин"),
("Map mode", ""), ("Map mode", "По начин със съответствие (map)"),
("Translate mode", "Режим на превод"), ("Translate mode", "По нчаин с превод"),
("Use permanent password", "Използвайте постоянна парола"), ("Use permanent password", "Използване на постоянна парола"),
("Use both passwords", "Използвайте и двете пароли"), ("Use both passwords", "Използване и на двете пароли"),
("Set permanent password", "Задайте постоянна парола"), ("Set permanent password", "Задаване постоянна парола"),
("Enable remote restart", "Разрешете отдалечено рестартиране"), ("Enable remote restart", "Разрешаване на отдалечен рестарт"),
("Restart remote device", "Рестартирайте отдалеченото устройство"), ("Restart remote device", "Рестартиране на отдалечено устройство"),
("Are you sure you want to restart", "Сигурни ли сте, че искате да рестартирате"), ("Are you sure you want to restart", "Сигурни ли сте, че искате да рестартирате"),
("Restarting remote device", "Рестартира се отдалечено устройство"), ("Restarting remote device", "Рестартиране на отдалечено устройство"),
("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"), ("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"),
("Copied", "Копирано"), ("Copied", "Преписано"),
("Exit Fullscreen", "Изход от цял екран"), ("Exit Fullscreen", "Изход от цял екран"),
("Fullscreen", "Цял екран"), ("Fullscreen", "Цял екран"),
("Mobile Actions", "Мобилни действия"), ("Mobile Actions", "Мобилни действия"),
@@ -334,10 +334,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Hide Toolbar", "Скриване на лентата с инструменти"), ("Hide Toolbar", "Скриване на лентата с инструменти"),
("Direct Connection", "Директна връзка"), ("Direct Connection", "Директна връзка"),
("Relay Connection", "Релейна връзка"), ("Relay Connection", "Релейна връзка"),
("Secure Connection", "Защитена връзка"), ("Secure Connection", "Сигурна връзка"),
("Insecure Connection", "Незащитена връзка"), ("Insecure Connection", "Несигурна връзка"),
("Scale original", "Оригинален мащаб"), ("Scale original", "Оригинален мащаб"),
("Scale adaptive", "Адаптивно мащабиране"), ("Scale adaptive", "Приспособимо мащабиране"),
("General", "Основен"), ("General", "Основен"),
("Security", "Сигурност"), ("Security", "Сигурност"),
("Theme", "Тема"), ("Theme", "Тема"),
@@ -345,128 +345,128 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Light Theme", "Светла тема"), ("Light Theme", "Светла тема"),
("Dark", "Тъмна"), ("Dark", "Тъмна"),
("Light", "Светла"), ("Light", "Светла"),
("Follow System", "Следвай системата"), ("Follow System", "Следвай система"),
("Enable hardware codec", "Активиране на хардуерен кодек"), ("Enable hardware codec", "Позволяване хардуерен кодек"),
("Unlock Security Settings", "Отключи настройките за сигурност"), ("Unlock Security Settings", "Отключи настройките за сигурност"),
("Enable audio", "Разрешете аудиото"), ("Enable audio", "Разрешете аудиото"),
("Unlock Network Settings", "Отключи мрежовите настройки"), ("Unlock Network Settings", "Отключи мрежовите настройки"),
("Server", "Сървър"), ("Server", "Сървър"),
("Direct IP Access", "Директен IP достъп"), ("Direct IP Access", "Пряк IP достъп"),
("Proxy", "Прокси"), ("Proxy", "Посредник (Proxy)"),
("Apply", "Приложи"), ("Apply", "Прилагане"),
("Disconnect all devices?", ""), ("Disconnect all devices?", "Разкачване на всички устройства"),
("Clear", "Изчисти"), ("Clear", "Изчистване"),
("Audio Input Device", "Аудио входно устройство"), ("Audio Input Device", "Аудио входно устройство"),
("Use IP Whitelisting", "Използвайте бял списък с IP адреси"), ("Use IP Whitelisting", "Използване бял списък с IP адреси"),
("Network", "Мрежа"), ("Network", "Мрежа"),
("Pin Toolbar", "Фиксиране на лентата с инструменти"), ("Pin Toolbar", "Закачане лента с инструменти"),
("Unpin Toolbar", "Откачване на лентата с инструменти"), ("Unpin Toolbar", "Откачюане лента с инструменти"),
("Recording", "Записване"), ("Recording", "Записване"),
("Directory", "Директория"), ("Directory", "Директория"),
("Automatically record incoming sessions", ""), ("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"),
("Change", "Промени"), ("Change", "Промяна"),
("Start session recording", ""), ("Start session recording", "Започванена запис"),
("Stop session recording", ""), ("Stop session recording", "Край на запис"),
("Enable recording session", ""), ("Enable recording session", "Позволяване запис"),
("Enable LAN discovery", "Активирайте откриване в LAN"), ("Enable LAN discovery", "Позволяване откриване във вътрешна мрежа"),
("Deny LAN discovery", "Забранете откриване в LAN"), ("Deny LAN discovery", "Забрана за откриване във вътрешна мрежа"),
("Write a message", "Напишете съобщение"), ("Write a message", "Напишете съобщение"),
("Prompt", "Подкана"), ("Prompt", "Подкана"),
("Please wait for confirmation of UAC...", ""), ("Please wait for confirmation of UAC...", "Моля изчакайте за потвърждение от UAC..."),
("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."), ("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."),
("Disconnected", "Прекъсната връзка"), ("Disconnected", "Прекъсната връзка"),
("Other", "Други"), ("Other", "Други"),
("Confirm before closing multiple tabs", ""), ("Confirm before closing multiple tabs", "Потвърждение преди затваряне на няколко раздела"),
("Keyboard Settings", "Настройки на клавиатурата"), ("Keyboard Settings", "Настройки на клавиатурата"),
("Full Access", "Пълен достъп"), ("Full Access", "Пълен достъп"),
("Screen Share", "Споделяне на екрана"), ("Screen Share", "Споделяне на екрана"),
("Wayland requires Ubuntu 21.04 or higher version.", ""), ("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 requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."),
("JumpLink", "Преглед"), ("JumpLink", "Препратка"),
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (Работете от страна на партньора)."), ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
("Show RustDesk", "Покажи RustDesk"), ("Show RustDesk", "Покажи RustDesk"),
("This PC", "Този компютър"), ("This PC", "Този компютър"),
("or", "или"), ("or", "или"),
("Continue with", "Продължи с"), ("Continue with", "Продължи с"),
("Elevate", "Повишаване"), ("Elevate", "Повишаване"),
("Zoom cursor", "Мащабиране на Курсор"), ("Zoom cursor", "Уголемяване курсор"),
("Accept sessions via password", "Приемайте сесии чрез парола"), ("Accept sessions via password", "Приемане сесии чрез парола"),
("Accept sessions via click", "Приемане на сесии чрез щракване"), ("Accept sessions via click", "Приемане сесии чрез цъкване"),
("Accept sessions via both", "Приемайте сесии и през двете"), ("Accept sessions via both", "Приемане сесии и по двата начина"),
("Please wait for the remote side to accept your session request...", ""), ("Please wait for the remote side to accept your session request...", "Моля, изчакайте докато другата страна приеме заявката за отдалечен достъп..."),
("One-time Password", "Еднократна парола"), ("One-time Password", "Еднократна парола"),
("Use one-time password", ""), ("Use one-time password", "Ползване на еднократна парола"),
("One-time password length", ""), ("One-time password length", "Дължина на еднократна парола"),
("Request access to your device", ""), ("Request access to your device", "Искане за достъп до ваше устройство"),
("Hide connection management window", ""), ("Hide connection management window", "Скриване на прозореца за управление на свързване"),
("hide_cm_tip", "Разрешете скриването само ако приемате сесии чрез парола и използвате постоянна парола"), ("hide_cm_tip", "Разрешаване скриване само ако се приемат сесии чрез постоянна парола"),
("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ("wayland_experiment_tip", "Поддръжката на Wayland е в експериментален стадий, моля, използвайте X11, ако се нуждаете от безконтролен достъп.."),
("Right click to select tabs", ""), ("Right click to select tabs", "Десен бутон за избор на раздел"),
("Skipped", "Пропуснато"), ("Skipped", "Пропуснато"),
("Add to address book", ""), ("Add to address book", "Добавяне към познати адреси"),
("Group", "Група"), ("Group", "Група"),
("Search", "Търсене"), ("Search", "Търсене"),
("Closed manually by web console", ""), ("Closed manually by web console", "Затворен ръчно от уеб конзола"),
("Local keyboard type", ""), ("Local keyboard type", "Тип на тукашната клавиатура"),
("Select local keyboard type", ""), ("Select local keyboard type", "Избор на тип на тукашната клавиатура"),
("software_render_tip", "Ако използвате графична карта Nvidia под Linux и отдалеченият прозорец се затваря веднага след свързване, превключването към драйвера Nouveau с отворен код и изборът да използвате софтуерно изобразяване може да помогне. Изисква се рестартиране на софтуера."), ("software_render_tip", "Ако използвате графична карта Nvidia под Linux и отдалеченият прозорец се затваря веднага след свързване, превключването към драйвера Nouveau с отворен код и изборът да използвате софтуерно изобразяване може да помогне. Изисква се рестартиране на софтуера."),
("Always use software rendering", ""), ("Always use software rendering", "Винаги ползвай софтуерно изграждане на картината"),
("config_input", "За да контролирате отдалечен работен плот с клавиатура, трябва да предоставите на RustDesk разрешения \"Input Monitoring\"."), ("config_input", "За да управлявате отдалечена среда с клавиатура, трябва да предоставите на RustDesk право за \"Input Monitoring\"."),
("config_microphone", "За да говорите дистанционно, трябва да предоставите на RustDesk разрешения \"Запис на звук\"."), ("config_microphone", "За да говорите отдалечено, трябва да предоставите на RustDesk право за \"Запис на звук\"."),
("request_elevation_tip", "Можете също така да поискате повишаване на привилегии, ако има някой от отдалечената страна."), ("request_elevation_tip", "Можете също така да поискате разширени права, ако има някой от отдалечената страна."),
("Wait", "Изчакайте"), ("Wait", "Изчакване"),
("Elevation Error", "Грешка при повишаване на привилегии"), ("Elevation Error", "Грешка при добвиане на разширени права"),
("Ask the remote user for authentication", ""), ("Ask the remote user for authentication", "Попитайте отдалечения потребител за удостоверяване"),
("Choose this if the remote account is administrator", ""), ("Choose this if the remote account is administrator", "Изберете това, ако отдалеченият потребител е администратор."),
("Transmit the username and password of administrator", ""), ("Transmit the username and password of administrator", "Предаване на потребителското име и паролата на администратора"),
("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиранят RustDesk."), ("still_click_uac_tip", "Все още изисква отдалеченият потребител да щракне върху OK в прозореца на UAC при стартиран RustDesk."),
("Request Elevation", "Поискайте повишаване на привилегии"), ("Request Elevation", "Поискайте разширени права"),
("wait_accept_uac_tip", "Моля, изчакайте отдалеченият потребител да приеме диалоговия прозорец на UAC."), ("wait_accept_uac_tip", "Моля, изчакайте отдалеченият потребител да приеме диалоговия прозорец на UAC."),
("Elevate successfully", ""), ("Elevate successfully", "Успешно получаване на разширени права"),
("uppercase", ""), ("uppercase", "големи букви"),
("lowercase", ""), ("lowercase", "малки букви"),
("digit", ""), ("digit", "цифра"),
("special character", ""), ("special character", "специален знак"),
("length>=8", ""), ("length>=8", "дължина>=8"),
("Weak", ""), ("Weak", "Слаба"),
("Medium", ""), ("Medium", "Средна"),
("Strong", ""), ("Strong", "Силна"),
("Switch Sides", "Сменете страните"), ("Switch Sides", "Размяна на страните"),
("Please confirm if you want to share your desktop?", ""), ("Please confirm if you want to share your desktop?", "Моля, потвърдете дали искате да споделите работното си пространство"),
("Display", ""), ("Display", "Екран"),
("Default View Style", "Стил на изглед по подразбиране"), ("Default View Style", "Стил на изглед по подразбиране"),
("Default Scroll Style", "Стил на превъртане по подразбиране"), ("Default Scroll Style", "Стил на превъртане по подразбиране"),
("Default Image Quality", "Качество на изображението по подразбиране"), ("Default Image Quality", "Качество на изображението по подразбиране"),
("Default Codec", "Кодек по подразбиране"), ("Default Codec", "Кодек по подразбиране"),
("Bitrate", "Битрейт"), ("Bitrate", "Скорост на предаване на данни (bitrate)"),
("FPS", "Кадри в секунда"), ("FPS", "Кадри в секунда"),
("Auto", "Автоматично"), ("Auto", "Автоматично"),
("Other Default Options", "Други опции по подразбиране"), ("Other Default Options", "Други опции по подразбиране"),
("Voice call", ""), ("Voice call", "Гласови обаждания"),
("Text chat", ""), ("Text chat", "Текстов разговор"),
("Stop voice call", ""), ("Stop voice call", "Прекратяване гласово обаждане"),
("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез реле. Освен това, ако искате да използвате реле при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез реле\" в картата на последните сесии, ако съществува."), ("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез реле. Освен това, ако искате да използвате реле при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез реле\" в картата на последните сесии, ако съществува."),
("Reconnect", "Свържете се отново"), ("Reconnect", "Повторно свързане"),
("Codec", "Кодек"), ("Codec", "Кодек"),
("Resolution", "Резолюция"), ("Resolution", "Разделителна способност"),
("No transfers in progress", "Не се извършват трансфери"), ("No transfers in progress", "Няма текущи прехвърляния"),
("Set one-time password length", ""), ("Set one-time password length", "Задаване дължаина на еднократна парола"),
("RDP Settings", "RDP настройки"), ("RDP Settings", "RDP настройки"),
("Sort by", "Сортирай по"), ("Sort by", "Подредба по"),
("New Connection", "Ново свързване"), ("New Connection", "Ново свързване"),
("Restore", ""), ("Restore", "Възстановяване"),
("Minimize", ""), ("Minimize", "Смаляване"),
("Maximize", ""), ("Maximize", "Уголемяване"),
("Your Device", "Вашето устройство"), ("Your Device", "Вашето устройство"),
("empty_recent_tip", "Ами сега, няма скорошни сесии!\nВреме е да планирате нова."), ("empty_recent_tip", "Ами сега, няма скорошни сесии!\nВреме е да планирате нова."),
("empty_favorite_tip", "Все още нямате любими връстници?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"), ("empty_favorite_tip", "Все още нямате любими връстници?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"),
("empty_lan_tip", "О, не, изглежда, че все още не сме открили връстници."), ("empty_lan_tip", "О, не, изглежда, че все още не сме открили връстници."),
("empty_address_book_tip", "Изглежда, че в момента няма изброени връстници във вашата адресна книга."), ("empty_address_book_tip", "Изглежда, че в момента няма изброени връстници във вашата адресна книга."),
("eg: admin", ""), ("eg: admin", "напр. admin"),
("Empty Username", "Празно потребителско име"), ("Empty Username", "Празно потребителско име"),
("Empty Password", "Празна парола"), ("Empty Password", "Празна парола"),
("Me", "Аз"), ("Me", "Мен"),
("identical_file_tip", "Този файл е идентичен с този на партньора."), ("identical_file_tip", "Файлът съвпада с този от другата страна."),
("show_monitors_tip", "Показване на мониторите в лентата с инструменти"), ("show_monitors_tip", "Показване на мониторите в лентата с инструменти"),
("View Mode", "Режим на преглед"), ("View Mode", "Режим на преглед"),
("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"), ("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"),
@@ -482,47 +482,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"), ("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"),
("No need to elevate", ""), ("No need to elevate", ""),
("System Sound", "Системен звук"), ("System Sound", "Системен звук"),
("Default", ""), ("Default", "По подразбиране"),
("New RDP", ""), ("New RDP", "Нов RDP"),
("Fingerprint", ""), ("Fingerprint", "Пръстов отпечатък"),
("Copy Fingerprint", "Копиране на пръстов отпечатък"), ("Copy Fingerprint", "Копиране на пръстов отпечатък"),
("no fingerprints", "Няма пръстови отпечатъци"), ("no fingerprints", "Няма пръстови отпечатъци"),
("Select a peer", ""), ("Select a peer", "Избери отдалечена страна"),
("Select peers", ""), ("Select peers", "Избери отдалечени страни"),
("Plugins", ""), ("Plugins", "Приставки"),
("Uninstall", ""), ("Uninstall", "Премахни"),
("Update", ""), ("Update", "Обновяване"),
("Enable", ""), ("Enable", "Позволяване"),
("Disable", ""), ("Disable", "Забрана"),
("Options", "Настроики"), ("Options", "Настроики"),
("resolution_original_tip", "Оригинална резолюция"), ("resolution_original_tip", "Оригинална разделителна способност"),
("resolution_fit_local_tip", "Напасване към локална разделителна способност"), ("resolution_fit_local_tip", "Приспособяване към тукашната разделителна способност"),
("resolution_custom_tip", "Персонализирана разделителна способност"), ("resolution_custom_tip", "Разделителна способност по свой избор"),
("Collapse toolbar", "Свиване на лентата с инструменти"), ("Collapse toolbar", "Свиване на лентата с инструменти"),
("Accept and Elevate", "Приемете и повишаване на привилегии"), ("Accept and Elevate", "Приемане и предоставяне на допълнителни права"),
("accept_and_elevate_btn_tooltip", "Приемете връзката и повишете UAC разрешенията."), ("accept_and_elevate_btn_tooltip", "Приемане на връзката предоставяне на UAC разрешения."),
("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за копиране изтече."), ("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за препис изтече."),
("Incoming connection", ""), ("Incoming connection", "Входяща връзка"),
("Outgoing connection", ""), ("Outgoing connection", "Изходяща връзка"),
("Exit", "Излез"), ("Exit", "Изход"),
("Open", "Отвори"), ("Open", "Отваряне"),
("logout_tip", "Сигурни ли сте, че искате да излезете?"), ("logout_tip", "Сигурни ли сте, че искате да излезете?"),
("Service", "Услуга"), ("Service", "Услуга"),
("Start", "Стартиране"), ("Start", "Стартиране"),
("Stop", "Спиране"), ("Stop", "Спиране"),
("exceed_max_devices", "Достигнахте максималния брой управлявани устройства."), ("exceed_max_devices", "Достигнахте максималния брой управлявани устройства."),
("Sync with recent sessions", ""), ("Sync with recent sessions", "Синхронизиране с последните сесии"),
("Sort tags", ""), ("Sort tags", "Подреди белези"),
("Open connection in new tab", ""), ("Open connection in new tab", "Разкриване на връзка в нов раздел"),
("Move tab to new window", ""), ("Move tab to new window", "Отделяне на раздела в нов прозорец"),
("Can not be empty", ""), ("Can not be empty", "Не може да е празно"),
("Already exists", ""), ("Already exists", "Вече съществува"),
("Change Password", "Промяна на паролата"), ("Change Password", "Промяна на парола"),
("Refresh Password", "Обнови паролата"), ("Refresh Password", "Обновяване парола"),
("ID", ""), ("ID", "Определител (ID)"),
("Grid View", "Мрежов изглед"), ("Grid View", "Мрежов изглед"),
("List View", "Списъчен изглед"), ("List View", "Списъчен изглед"),
("Select", ""), ("Select", "Избиране"),
("Toggle Tags", "Превключване на етикети"), ("Toggle Tags", "Превключване на етикети"),
("pull_ab_failed_tip", "Неуспешно опресняване на адресната книга"), ("pull_ab_failed_tip", "Неуспешно опресняване на адресната книга"),
("push_ab_failed_tip", "Неуспешно синхронизиране на адресната книга със сървъра"), ("push_ab_failed_tip", "Неуспешно синхронизиране на адресната книга със сървъра"),
@@ -530,119 +530,122 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Change Color", "Промяна на цвета"), ("Change Color", "Промяна на цвета"),
("Primary Color", "Основен цвят"), ("Primary Color", "Основен цвят"),
("HSV Color", "HSV цвят"), ("HSV Color", "HSV цвят"),
("Installation Successful!", "Успешна инсталация!"), ("Installation Successful!", "Успешно поставяне!"),
("Installation failed!", ""), ("Installation failed!", "Провал при поставяне"),
("Reverse mouse wheel", ""), ("Reverse mouse wheel", "Обърнато колелото на мишката"),
("{} sessions", ""), ("{} sessions", "{} сесии"),
("scam_title", "Възможно е да сте ИЗМАМЕНИ!"), ("scam_title", "Възможно е да сте ИЗМАМЕНИ!"),
("scam_text1", "Ако разговаряте по телефона с някой, когото НЕ ПОЗНАВАТЕ и НЯМАТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да стартирате услугата, не продължавайте и затворете незабавно."), ("scam_text1", "Ако разговаряте по телефона с някой, когото НЕ ПОЗНАВАТЕ и НЯМАТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да стартирате услугата, не продължавайте и затворете незабавно."),
("scam_text2", "Те вероятно са измамник, който се опитва да открадне вашите пари или друга лична информация."), ("scam_text2", "Те вероятно са измамник, който се опитва да открадне вашите пари или друга лична информация."),
("Don't show again", "Не показвай отново"), ("Don't show again", "Не показвай отново"),
("I Agree", ""), ("I Agree", "Съгласен"),
("Decline", ""), ("Decline", "Отказвам"),
("Timeout in minutes", ""), ("Timeout in minutes", "Време за отговор в минути"),
("auto_disconnect_option_tip", "Автоматично затваряне на входящите сесии при неактивност на потребителя"), ("auto_disconnect_option_tip", "Автоматично затваряне на входящите сесии при неактивност на потребителя"),
("Connection failed due to inactivity", "Автоматично прекъсване на връзката поради неактивност"), ("Connection failed due to inactivity", "Автоматично прекъсване на връзката поради неактивност"),
("Check for software update on startup", ""), ("Check for software update on startup", ""),
("upgrade_rustdesk_server_pro_to_{}_tip", "Моля обновете RustDesk Server Pro на версия {} или по-нова!"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Моля обновете RustDesk Server Pro на версия {} или по-нова!"),
("pull_group_failed_tip", "Неуспешно опресняване на групата"), ("pull_group_failed_tip", "Неуспешно опресняване на групата"),
("Filter by intersection", ""), ("Filter by intersection", "Отсяване по пресичане"),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", "Спри фоновото изображение по време на входящи сесии"),
("Test", ""), ("Test", "Проверка"),
("display_is_plugged_out_msg", "Дисплеят е изключен, превключете на първия монитор."), ("display_is_plugged_out_msg", "Дисплеят е изключен, превключете на първия монитор."),
("No displays", ""), ("No displays", "Няма екрани"),
("Open in new window", ""), ("Open in new window", "Отваряне в нов прозорец"),
("Show displays as individual windows", ""), ("Show displays as individual windows", "Показване на екраните в отделни прозорци"),
("Use all my displays for the remote session", ""), ("Use all my displays for the remote session", "Използване на всички тукашни екрани за отдалечена работа"),
("selinux_tip", "SELinux е активиран на вашето устройство, което може да попречи на RustDesk да работи правилно като контролирана страна."), ("selinux_tip", "SELinux е активиран на вашето устройство, което може да попречи на RustDesk да работи правилно като контролирана страна."),
("Change view", ""), ("Change view", "Промяна изглед"),
("Big tiles", ""), ("Big tiles", "Големи заглавия"),
("Small tiles", ""), ("Small tiles", "Малки заглавия"),
("List", ""), ("List", "Списък"),
("Virtual display", ""), ("Virtual display", "Виртуален екран"),
("Plug out all", ""), ("Plug out all", "Изтръгване на всички"),
("True color (4:4:4)", ""), ("True color (4:4:4)", ""),
("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"), ("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"),
("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (<domain>:<port>).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (<id>@<server_address >?key=<key_value>), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"<id>@public\" , ключът не е необходим за публичен сървър"), ("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (<domain>:<port>).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (<id>@<server_address >?key=<key_value>), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"<id>@public\" , ключът не е необходим за публичен сървър"),
("privacy_mode_impl_mag_tip", "Режим 1"), ("privacy_mode_impl_mag_tip", "Режим 1"),
("privacy_mode_impl_virtual_display_tip", "Режим 2"), ("privacy_mode_impl_virtual_display_tip", "Режим 2"),
("Enter privacy mode", ""), ("Enter privacy mode", "Влизане в поверителен режим"),
("Exit privacy mode", ""), ("Exit privacy mode", "Изход от поверителен режим"),
("idd_not_support_under_win10_2004_tip", "Индиректен драйвер за дисплей не се поддържа. Изисква се Windows 10, версия 2004 или по-нова."), ("idd_not_support_under_win10_2004_tip", "Индиректен драйвер за дисплей не се поддържа. Изисква се Windows 10, версия 2004 или по-нова."),
("input_source_1_tip", "Входен източник 1"), ("input_source_1_tip", "Входен източник 1"),
("input_source_2_tip", "Входен източник 2"), ("input_source_2_tip", "Входен източник 2"),
("Swap control-command key", ""), ("Swap control-command key", ""),
("swap-left-right-mouse", "Разменете левия и десния бутон на мишката"), ("swap-left-right-mouse", "Размяна на копчетата на мишката"),
("2FA code", "Код за Двуфакторна удостоверяване"), ("2FA code", "Код за Двуфакторно удостоверяване"),
("More", "Повече"), ("More", "Повече"),
("enable-2fa-title", "Активиране на двуфакторно удостоверяване"), ("enable-2fa-title", "Позволяване на двуфакторно удостоверяване"),
("enable-2fa-desc", "Моля, настройте вашия удостоверител сега. Можете да използвате приложение за удостоверяване като Authy, Microsoft или Google Authenticator на вашия телефон или настолен компютър.\n\nСканирайте QR кода с вашето приложение и въведете кода, който приложението ви показва, за да активирате двуфакторно удостоверяване."), ("enable-2fa-desc", "Моля, настройте вашия удостоверител сега. Можете да използвате приложение за удостоверяване като Authy, Microsoft или Google Authenticator на вашия телефон или настолен компютър.\n\nСканирайте QR кода с вашето приложение и въведете кода, който приложението ви показва, за да активирате двуфакторно удостоверяване."),
("wrong-2fa-code", "е може да се потвърди кодът. Проверете дали настройките за код и локалното време са правилни"), ("wrong-2fa-code", "е може да се потвърди кодът. Проверете дали настройките за код и локалното време са правилни"),
("enter-2fa-title", "Двуфакторно удостоверяване"), ("enter-2fa-title", "Двуфакторно удостоверяване"),
("Email verification code must be 6 characters.", ""), ("Email verification code must be 6 characters.", "Кодът за проверка следва да е с дължина 6 знака."),
("2FA code must be 6 digits.", ""), ("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"),
("Multiple Windows sessions found", ""), ("Multiple Windows sessions found", "Установени са няколко Windwos сесии"),
("Please select the session you want to connect to", ""), ("Please select the session you want to connect to", "Моля определете сесия към която искате да се свърженете"),
("powered_by_me", ""), ("powered_by_me", ""),
("outgoing_only_desk_tip", ""), ("outgoing_only_desk_tip", ""),
("preset_password_warning", ""), ("preset_password_warning", ""),
("Security Alert", ""), ("Security Alert", "Предупреждение за сигурност"),
("My address book", ""), ("My address book", "Моята адресна книга"),
("Personal", ""), ("Personal", "Личен"),
("Owner", ""), ("Owner", "Собственик"),
("Set shared password", ""), ("Set shared password", "Определяне споделена парола"),
("Exist in", ""), ("Exist in", "Съществува в"),
("Read-only", ""), ("Read-only", "Само четене"),
("Read/Write", ""), ("Read/Write", "Писане/четене"),
("Full Control", ""), ("Full Control", "Пълен контрол"),
("share_warning_tip", ""), ("share_warning_tip", ""),
("Everyone", ""), ("Everyone", "Всички"),
("ab_web_console_tip", ""), ("ab_web_console_tip", ""),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", ""),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""),
("Follow remote cursor", ""), ("Follow remote cursor", "Следвай отдалечения курсор"),
("Follow remote window focus", ""), ("Follow remote window focus", "Следвай фокуса на отдалечените прозорци"),
("default_proxy_tip", ""), ("default_proxy_tip", ""),
("no_audio_input_device_tip", ""), ("no_audio_input_device_tip", ""),
("Incoming", ""), ("Incoming", "Входящ"),
("Outgoing", ""), ("Outgoing", "Изходящ"),
("Clear Wayland screen selection", ""), ("Clear Wayland screen selection", "Изчистване избор на Wayland екран"),
("clear_Wayland_screen_selection_tip", ""), ("clear_Wayland_screen_selection_tip", ""),
("confirm_clear_Wayland_screen_selection_tip", ""), ("confirm_clear_Wayland_screen_selection_tip", ""),
("android_new_voice_call_tip", ""), ("android_new_voice_call_tip", ""),
("texture_render_tip", ""), ("texture_render_tip", ""),
("Use texture rendering", ""), ("Use texture rendering", "Използвай текстово изграждане"),
("Floating window", ""), ("Floating window", "Плаващ прозорец"),
("floating_window_tip", ""), ("floating_window_tip", ""),
("Keep screen on", ""), ("Keep screen on", "Запази екранът включен"),
("Never", ""), ("Never", "Никога"),
("During controlled", ""), ("During controlled", "Докато е обект на управление"),
("During service is on", ""), ("During service is on", "Докато услугата е включена"),
("Capture screen using DirectX", ""), ("Capture screen using DirectX", "Снемай екрана ползвайки DirectX"),
("Back", "Назад"), ("Back", "Назад"),
("Apps", ""), ("Apps", "Приложения"),
("Volume up", ""), ("Volume up", "Усилване звук"),
("Volume down", ""), ("Volume down", "Намаляне звук"),
("Power", ""), ("Power", "Мощност"),
("Telegram bot", ""), ("Telegram bot", "Телеграм бот"),
("enable-bot-tip", ""), ("enable-bot-tip", ""),
("enable-bot-desc", ""), ("enable-bot-desc", ""),
("cancel-2fa-confirm-tip", ""), ("cancel-2fa-confirm-tip", ""),
("cancel-bot-confirm-tip", ""), ("cancel-bot-confirm-tip", ""),
("About RustDesk", ""), ("About RustDesk", "Относно RustDesk"),
("Send clipboard keystrokes", ""), ("Send clipboard keystrokes", ""),
("network_error_tip", ""), ("network_error_tip", ""),
("Unlock with PIN", ""), ("Unlock with PIN", "Отключване с PIN"),
("Requires at least {} characters", ""), ("Requires at least {} characters", ""),
("Wrong PIN", ""), ("Wrong PIN", "Грешен PIN"),
("Set PIN", ""), ("Set PIN", "Избор PIN"),
("Enable trusted devices", ""), ("Enable trusted devices", "Позволяване доверени устройства"),
("Manage trusted devices", ""), ("Manage trusted devices", "Управление доверени устройства"),
("Platform", ""), ("Platform", "Платформа"),
("Days remaining", ""), ("Days remaining", "Оставащи дни"),
("enable-trusted-devices-tip", ""), ("enable-trusted-devices-tip", ""),
("Parent directory", ""), ("Parent directory", ""),
("Resume", ""), ("Resume", "Възобновяване"),
("Invalid file name", ""), ("Invalid file name", "Невалидно име за файл"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", ""), ("Parent directory", ""),
("Resume", ""), ("Resume", ""),
("Invalid file name", ""), ("Invalid file name", ""),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "父目录"), ("Parent directory", "父目录"),
("Resume", "继续"), ("Resume", "继续"),
("Invalid file name", "无效文件名"), ("Invalid file name", "无效文件名"),
("one-way-file-transfer-tip", "被控端启用了单向文件传输"),
("Authentication Required", "需要身份验证"),
("Authenticate", "认证"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -641,8 +641,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Platform", "Platforma"), ("Platform", "Platforma"),
("Days remaining", "Zbývajících dnů"), ("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"), ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"),
("Parent directory", ""), ("Parent directory", "Rodičovský adresář"),
("Resume", ""), ("Resume", "Pokračovat"),
("Invalid file name", ""), ("Invalid file name", "Nesprávný název souboru"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[ [
("Status", "Status"), ("Status", "Status"),
("Your Desktop", "Dit skrivebord"), ("Your Desktop", "Dit skrivebord"),
("desk_tip", "Du kan 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"), ("Password", "Adgangskode"),
("Ready", "Klar"), ("Ready", "Klar"),
("Established", "Etableret"), ("Established", "Etableret"),
@@ -38,18 +38,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Stop service", "Sluk for forbindelsesserveren"), ("Stop service", "Sluk for forbindelsesserveren"),
("Change ID", "Ændr ID"), ("Change ID", "Ændr ID"),
("Your new ID", "Dit nye ID"), ("Your new ID", "Dit nye ID"),
("length %min% to %max%", ""), ("length %min% to %max%", "længde %min% til %max%"),
("starts with a letter", ""), ("starts with a letter", "starter med ét bogstav"),
("allowed characters", ""), ("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. Længde mellem 6 og 16."), ("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"), ("Website", "Hjemmeside"),
("About", "Om"), ("About", "Om"),
("Slogan_tip", ""), ("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"),
("Privacy Statement", ""), ("Privacy Statement", "Privatlivspolitik"),
("Mute", "Sluk for mikrofonen"), ("Mute", "Sluk for mikrofonen"),
("Build Date", ""), ("Build Date", "Build dato"),
("Version", ""), ("Version", "Version"),
("Home", ""), ("Home", "Hjem"),
("Audio Input", "Lydinput"), ("Audio Input", "Lydinput"),
("Enhancements", "Forbedringer"), ("Enhancements", "Forbedringer"),
("Hardware Codec", "Hardware-codec"), ("Hardware Codec", "Hardware-codec"),
@@ -120,8 +120,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Original", "Original"), ("Original", "Original"),
("Shrink", "Krymp"), ("Shrink", "Krymp"),
("Stretch", "Stræk ud"), ("Stretch", "Stræk ud"),
("Scrollbar", "Rullebar"), ("Scrollbar", "Scrollbar"),
("ScrollAuto", "Auto-rul"), ("ScrollAuto", "Auto-scroll"),
("Good image quality", "God billedkvalitet"), ("Good image quality", "God billedkvalitet"),
("Balanced", "Afbalanceret"), ("Balanced", "Afbalanceret"),
("Optimize reaction time", "Optimeret responstid"), ("Optimize reaction time", "Optimeret responstid"),
@@ -139,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remote desktop is offline", "Fjernskrivebord er offline"), ("Remote desktop is offline", "Fjernskrivebord er offline"),
("Key mismatch", "Nøgle uoverensstemmelse"), ("Key mismatch", "Nøgle uoverensstemmelse"),
("Timeout", "Timeout"), ("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 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"), ("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"),
("Set Password", "Indstil adgangskode"), ("Set Password", "Indstil adgangskode"),
("OS Password", "Operativsystemadgangskode"), ("OS Password", "Operativsystemadgangskode"),
@@ -218,7 +218,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Remember me", "Husk mig"), ("Remember me", "Husk mig"),
("Trust this device", "Husk denne enhed"), ("Trust this device", "Husk denne enhed"),
("Verification code", "Verifikationskode"), ("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"), ("Logout", "Logger af"),
("Tags", "Nøgleord"), ("Tags", "Nøgleord"),
("Search ID", "Søg efter ID"), ("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"), ("Username missed", "Glemt brugernavn"),
("Password missed", "Glemt kodeord"), ("Password missed", "Glemt kodeord"),
("Wrong credentials", "Forkerte registreringsdata"), ("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"), ("Edit Tag", "Rediger nøgleord"),
("Forget Password", "Glem adgangskoden"), ("Forget Password", "Glem adgangskoden"),
("Favorites", "Favoritter"), ("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?"), ("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"),
("Download new version", "Download ny version"), ("Download new version", "Download ny version"),
("Touch mode", "Touch-tilstand"), ("Touch mode", "Touch-tilstand"),
("Mouse mode", "Musse-tilstand"), ("Mouse mode", "Muse-tilstand"),
("One-Finger Tap", "En-finger-tryk"), ("One-Finger Tap", "En-finger-tryk"),
("Left Mouse", "Venstre mus"), ("Left Mouse", "Venstre mus"),
("One-Long Tap", "Tryk og hold med en finger"), ("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_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_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_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_start_service_tip", "Tryk [Start tjeneste] eller aktivér [Skærmoptagelse] tilladelse for at dele skærmen."),
("android_permission_may_not_change_tip", ""), ("android_permission_may_not_change_tip", "Rettigheder til oprettede forbindelser ændres ikke med det samme før der forbindelsen genoprettes."),
("Account", "Konto"), ("Account", "Konto"),
("Overwrite", "Overskriv"), ("Overwrite", "Overskriv"),
("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"), ("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"), ("Language", "Sprog"),
("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"),
("Ignore Battery Optimizations", "Ignorér betteri optimeringer"), ("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 on boot", "Start under opstart"),
("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"), ("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"), ("Connection not allowed", "Forbindelse ikke tilladt"),
@@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Map mode", "Kortmodus"), ("Map mode", "Kortmodus"),
("Translate mode", "Oversættelsesmodus"), ("Translate mode", "Oversættelsesmodus"),
("Use permanent password", "Brug permanent adgangskode"), ("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"), ("Set permanent password", "Sæt permanent adgangskode"),
("Enable remote restart", "Aktivér fjerngenstart"), ("Enable remote restart", "Aktivér fjerngenstart"),
("Restart remote device", "Genstart fjernenhed"), ("Restart remote device", "Genstart fjernenhed"),
@@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Ratio", "Forhold"), ("Ratio", "Forhold"),
("Image Quality", "Billedkvalitet"), ("Image Quality", "Billedkvalitet"),
("Scroll Style", "Rullestil"), ("Scroll Style", "Rullestil"),
("Show Toolbar", ""), ("Show Toolbar", "Vis værktøjslinje"),
("Hide Toolbar", ""), ("Hide Toolbar", "Skjul værktøjslinje"),
("Direct Connection", "Direkte forbindelse"), ("Direct Connection", "Direkte forbindelse"),
("Relay Connection", "Viderestillingsforbindelse"), ("Relay Connection", "Viderestillingsforbindelse"),
("Secure Connection", "Sikker forbindelse"), ("Secure Connection", "Sikker forbindelse"),
@@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input Device", "Lydindgangsenhed"), ("Audio Input Device", "Lydindgangsenhed"),
("Use IP Whitelisting", "Brug IP Whitelisting"), ("Use IP Whitelisting", "Brug IP Whitelisting"),
("Network", "Netværk"), ("Network", "Netværk"),
("Pin Toolbar", ""), ("Pin Toolbar", "Fastgør værktøjslinjen"),
("Unpin Toolbar", ""), ("Unpin Toolbar", "Frigiv værktøjslinjen"),
("Recording", "Optager"), ("Recording", "Optager"),
("Directory", "Mappe"), ("Directory", "Mappe"),
("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"),
@@ -368,15 +368,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Start session recording", "Start sessionsoptagelse"), ("Start session recording", "Start sessionsoptagelse"),
("Stop session recording", "Stop sessionsoptagelse"), ("Stop session recording", "Stop sessionsoptagelse"),
("Enable recording session", "Aktivér optagelsessession"), ("Enable recording session", "Aktivér optagelsessession"),
("Enable LAN discovery", "Aktivér LAN Discovery"), ("Enable LAN discovery", "Aktivér opdagelse via det lokale netværk"),
("Deny LAN discovery", "Afvis LAN Discovery"), ("Deny LAN discovery", "Afvis opdagelse via det lokale netværk"),
("Write a message", "Skriv en besked"), ("Write a message", "Skriv en besked"),
("Prompt", "Prompt"), ("Prompt", "Prompt"),
("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."), ("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"), ("Disconnected", "Afbrudt"),
("Other", "Andre"), ("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"), ("Keyboard Settings", "Tastaturindstillinger"),
("Full Access", "Fuld adgang"), ("Full Access", "Fuld adgang"),
("Screen Share", "Skærmdeling"), ("Screen Share", "Skærmdeling"),
@@ -399,8 +399,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("One-time password length", "Engangskode længde"), ("One-time password length", "Engangskode længde"),
("Request access to your device", "Efterspørg adgang til din enhed"), ("Request access to your device", "Efterspørg adgang til din enhed"),
("Hide connection management window", "Skjul forbindelseshåndteringsvindue"), ("Hide connection management window", "Skjul forbindelseshåndteringsvindue"),
("hide_cm_tip", ""), ("hide_cm_tip", "Tillad at skjule, hvis der kun forbindes ved brug af midlertidige og permanente adgangskoder"),
("wayland_experiment_tip", ""), ("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"), ("Right click to select tabs", "Højreklik for at vælge faner"),
("Skipped", "Sprunget over"), ("Skipped", "Sprunget over"),
("Add to address book", "Tilføj til adressebog"), ("Add to address book", "Tilføj til adressebog"),
@@ -409,19 +409,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by web console", "Lukket ned manuelt af webkonsollen"), ("Closed manually by web console", "Lukket ned manuelt af webkonsollen"),
("Local keyboard type", "Lokal tastatur type"), ("Local keyboard type", "Lokal tastatur type"),
("Select local keyboard type", "Vælg 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"), ("Always use software rendering", "Brug altid software rendering"),
("config_input", ""), ("config_input", "For at styre fjernskrivebordet med tastaturet, skal du give Rustdesk rettigheder til at optage tastetryk"),
("config_microphone", ""), ("config_microphone", "For at tale sammen over fjernstyring, skal du give RustDesk rettigheder til at optage lyd"),
("request_elevation_tip", ""), ("request_elevation_tip", "Du kan også spørge om elevationsrettigheder, hvis der er nogen i nærheden af fjernenheden."),
("Wait", "Vent"), ("Wait", "Vent"),
("Elevation Error", "Elevationsfejl"), ("Elevation Error", "Elevationsfejl"),
("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"), ("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"), ("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"), ("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"), ("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"), ("Elevate successfully", "Elevation lykkedes"),
("uppercase", "store bogstaver"), ("uppercase", "store bogstaver"),
("lowercase", "små bogstaver"), ("lowercase", "små bogstaver"),
@@ -435,7 +435,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?"), ("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"),
("Display", "Visning"), ("Display", "Visning"),
("Default View Style", "Standard visningsstil"), ("Default View Style", "Standard visningsstil"),
("Default Scroll Style", "Standard rulle stil"), ("Default Scroll Style", "Standard scrollestil"),
("Default Image Quality", "Standard billedkvalitet"), ("Default Image Quality", "Standard billedkvalitet"),
("Default Codec", "Standard codec"), ("Default Codec", "Standard codec"),
("Bitrate", "Bitrate"), ("Bitrate", "Bitrate"),
@@ -445,7 +445,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Stemmeopkald"), ("Voice call", "Stemmeopkald"),
("Text chat", "Tekstchat"), ("Text chat", "Tekstchat"),
("Stop voice call", "Stop stemmeopkald"), ("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"), ("Reconnect", "Genopret"),
("Codec", "Codec"), ("Codec", "Codec"),
("Resolution", "Opløsning"), ("Resolution", "Opløsning"),
@@ -458,191 +458,194 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Minimize", "Minimér"), ("Minimize", "Minimér"),
("Maximize", "Maksimér"), ("Maximize", "Maksimér"),
("Your Device", "Din enhed"), ("Your Device", "Din enhed"),
("empty_recent_tip", ""), ("empty_recent_tip", "Ups, ingen seneste sessioner!\nTid til at oprette en ny."),
("empty_favorite_tip", ""), ("empty_favorite_tip", "Ingen yndlings modparter endnu?\nLad os finde én at forbinde til, og tilføje den til dine favoritter!"),
("empty_lan_tip", ""), ("empty_lan_tip", "Åh nej, det ser ud til, at vi ikke kunne finde nogen modparter endnu."),
("empty_address_book_tip", ""), ("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"), ("eg: admin", "fx: admin"),
("Empty Username", "Tom brugernavn"), ("Empty Username", "Tom brugernavn"),
("Empty Password", "Tom adgangskode"), ("Empty Password", "Tom adgangskode"),
("Me", "Mig"), ("Me", "Mig"),
("identical_file_tip", ""), ("identical_file_tip", "Denne fil er identisk med modpartens."),
("show_monitors_tip", ""), ("show_monitors_tip", "Vis skærme i værktøjsbjælken"),
("View Mode", ""), ("View Mode", "Visningstilstand"),
("login_linux_tip", ""), ("login_linux_tip", "Du skal logge på en fjernstyret Linux konto for at aktivere en X skrivebordssession"),
("verify_rustdesk_password_tip", ""), ("verify_rustdesk_password_tip", "Bekræft RustDesk adgangskode"),
("remember_account_tip", ""), ("remember_account_tip", "Husk denne konto"),
("os_account_desk_tip", ""), ("os_account_desk_tip", "Denne konto benyttes til at logge på fjernsystemet, og aktivere skrivebordssessionen i hovedløs tilstand"),
("OS Account", ""), ("OS Account", "Styresystem konto"),
("another_user_login_title_tip", ""), ("another_user_login_title_tip", "En anden bruger er allerede logget ind"),
("another_user_login_text_tip", ""), ("another_user_login_text_tip", "Frakobl"),
("xorg_not_found_title_tip", ""), ("xorg_not_found_title_tip", "Xorg ikke fundet"),
("xorg_not_found_text_tip", ""), ("xorg_not_found_text_tip", "Installér venlist Xorg"),
("no_desktop_title_tip", ""), ("no_desktop_title_tip", "Intet skrivebordsmiljø er tilgængeligt"),
("no_desktop_text_tip", ""), ("no_desktop_text_tip", "Installér venligst GNOME skrivebordet"),
("No need to elevate", ""), ("No need to elevate", "Ingen grund til at elevere"),
("System Sound", ""), ("System Sound", "Systemlyd"),
("Default", ""), ("Default", "Standard"),
("New RDP", ""), ("New RDP", "Ny RDP"),
("Fingerprint", ""), ("Fingerprint", "Fingeraftryk"),
("Copy Fingerprint", ""), ("Copy Fingerprint", "Kopiér fingeraftryk"),
("no fingerprints", ""), ("no fingerprints", "Ingen fingeraftryk"),
("Select a peer", ""), ("Select a peer", "Vælg en peer"),
("Select peers", ""), ("Select peers", "Vælg peers"),
("Plugins", ""), ("Plugins", "Plugins"),
("Uninstall", ""), ("Uninstall", "Afinstallér"),
("Update", ""), ("Update", "Opdatér"),
("Enable", ""), ("Enable", "Aktivér"),
("Disable", ""), ("Disable", "Deaktivér"),
("Options", ""), ("Options", "Valgmuligheder"),
("resolution_original_tip", ""), ("resolution_original_tip", "Original skærmopløsning"),
("resolution_fit_local_tip", ""), ("resolution_fit_local_tip", "Tilpas lokal skærmopløsning"),
("resolution_custom_tip", ""), ("resolution_custom_tip", "Bruger-tilpasset skærmopløsning"),
("Collapse toolbar", ""), ("Collapse toolbar", "Skjul værktøjsbjælke"),
("Accept and Elevate", ""), ("Accept and Elevate", "Acceptér og elevér"),
("accept_and_elevate_btn_tooltip", ""), ("accept_and_elevate_btn_tooltip", "Acceptér forbindelsen og elevér UAC tilladelser"),
("clipboard_wait_response_timeout_tip", ""), ("clipboard_wait_response_timeout_tip", "Tiden for at vente på en kopieringsforespørgsel udløb"),
("Incoming connection", ""), ("Incoming connection", "Indgående forbindelse"),
("Outgoing connection", ""), ("Outgoing connection", "Udgående forbindelse"),
("Exit", ""), ("Exit", "Afslut"),
("Open", ""), ("Open", "Åben"),
("logout_tip", ""), ("logout_tip", "Er du sikker på at du vil logge af?"),
("Service", ""), ("Service", "Tjeneste"),
("Start", ""), ("Start", "Start"),
("Stop", ""), ("Stop", "Stop"),
("exceed_max_devices", ""), ("exceed_max_devices", "Du har nået det maksimale antal håndtérbare enheder."),
("Sync with recent sessions", ""), ("Sync with recent sessions", "Synkronisér med tidligere sessioner"),
("Sort tags", ""), ("Sort tags", "Sortér nøgleord"),
("Open connection in new tab", ""), ("Open connection in new tab", "Åbn forbindelse i en ny fane"),
("Move tab to new window", ""), ("Move tab to new window", "Flyt fane i et nyt vindue"),
("Can not be empty", ""), ("Can not be empty", "Kan ikke være tom"),
("Already exists", ""), ("Already exists", "Findes allerede"),
("Change Password", ""), ("Change Password", "Skift adgangskode"),
("Refresh Password", ""), ("Refresh Password", "Genopfrisk adgangskode"),
("ID", ""), ("ID", "ID"),
("Grid View", ""), ("Grid View", "Gittervisning"),
("List View", ""), ("List View", "Listevisning"),
("Select", ""), ("Select", "Vælg"),
("Toggle Tags", ""), ("Toggle Tags", "Slå nøgleord til/fra"),
("pull_ab_failed_tip", ""), ("pull_ab_failed_tip", "Opdatering af adressebog mislykkedes"),
("push_ab_failed_tip", ""), ("push_ab_failed_tip", "Synkronisering af adressebog til serveren mislykkedes"),
("synced_peer_readded_tip", ""), ("synced_peer_readded_tip", "Enhederne, som var til stede i de seneste sessioner, vil blive synkroniseret tilbage til adressebogen."),
("Change Color", ""), ("Change Color", "Skift farve"),
("Primary Color", ""), ("Primary Color", "Primær farve"),
("HSV Color", ""), ("HSV Color", "HSV farve"),
("Installation Successful!", ""), ("Installation Successful!", "Installation fuldført!"),
("Installation failed!", ""), ("Installation failed!", "Installation mislykkedes!"),
("Reverse mouse wheel", ""), ("Reverse mouse wheel", "Invertér musehjul"),
("{} sessions", ""), ("{} sessions", "{} sessioner"),
("scam_title", ""), ("scam_title", "ADVARSEL: Du kan blive SVINDLET!"),
("scam_text1", ""), ("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", ""), ("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", ""), ("Don't show again", "Vis ikke igen"),
("I Agree", ""), ("I Agree", "Jeg accepterer"),
("Decline", ""), ("Decline", "Afvis"),
("Timeout in minutes", ""), ("Timeout in minutes", "Udløbstid i minutter"),
("auto_disconnect_option_tip", ""), ("auto_disconnect_option_tip", "Luk automatisk indkommende sessioner ved inaktivitet"),
("Connection failed due to inactivity", ""), ("Connection failed due to inactivity", "Forbindelsen blev afbrudt grundet inaktivitet"),
("Check for software update on startup", ""), ("Check for software update on startup", "Søg efter opdateringer ved opstart"),
("upgrade_rustdesk_server_pro_to_{}_tip", ""), ("upgrade_rustdesk_server_pro_to_{}_tip", "Opgradér venligst RustDesk Server Pro til version {} eller nyere!"),
("pull_group_failed_tip", ""), ("pull_group_failed_tip", "Genindlæsning af gruppe mislykkedes"),
("Filter by intersection", ""), ("Filter by intersection", "Filtrér efter intersection"),
("Remove wallpaper during incoming sessions", ""), ("Remove wallpaper during incoming sessions", "Skjul baggrundsskærm ved indgående forbindelser"),
("Test", ""), ("Test", "Test"),
("display_is_plugged_out_msg", ""), ("display_is_plugged_out_msg", "Skærmen er slukket, skift til den første skærm."),
("No displays", ""), ("No displays", "Ingen skærme"),
("Open in new window", ""), ("Open in new window", "Åbn i et nyt vindue"),
("Show displays as individual windows", ""), ("Show displays as individual windows", "Vis skærme som selvstændige vinduer"),
("Use all my displays for the remote session", ""), ("Use all my displays for the remote session", "Brug alle mine skærme til fjernforbindelsen"),
("selinux_tip", ""), ("selinux_tip", "SELinux er aktiveret på din enhed, som kan forhindre RustDesk i at køre normalt."),
("Change view", ""), ("Change view", "Skift visning"),
("Big tiles", ""), ("Big tiles", "Store fliser"),
("Small tiles", ""), ("Small tiles", "Små fliser"),
("List", ""), ("List", "Liste"),
("Virtual display", ""), ("Virtual display", "Virtuel skærm"),
("Plug out all", ""), ("Plug out all", "Frakobl alt"),
("True color (4:4:4)", ""), ("True color (4:4:4)", "True color (4:4:4)"),
("Enable blocking user input", ""), ("Enable blocking user input", "Aktivér blokering af brugerstyring"),
("id_input_tip", ""), ("id_input_tip", "Du kan indtaste ét ID, en direkte IP adresse, eller et domæne med en port (<domæne>:<port>).\nHvis du ønsker at forbinde til en enhed på en anden server, tilføj da server adressen (<id>@<server_adresse>?key=<nøgle>), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker adgang til en enhed på en offentlig server, indtast venligst \"<id>@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", ""), ("privacy_mode_impl_mag_tip", "Tilstand 1"),
("privacy_mode_impl_virtual_display_tip", ""), ("privacy_mode_impl_virtual_display_tip", "Tilstand 2"),
("Enter privacy mode", ""), ("Enter privacy mode", "Start privatlivstilstand"),
("Exit privacy mode", ""), ("Exit privacy mode", "Afslut privatlivstilstand"),
("idd_not_support_under_win10_2004_tip", ""), ("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_source_1_tip", "Input kilde 1"),
("input_source_2_tip", ""), ("input_source_2_tip", "Input kilde 2"),
("Swap control-command key", ""), ("Swap control-command key", "Byt rundt på Control & Command tasterne"),
("swap-left-right-mouse", ""), ("swap-left-right-mouse", "Byt rundt på venstre og højre musetaster"),
("2FA code", ""), ("2FA code", "To-faktor kode"),
("More", ""), ("More", "Mere"),
("enable-2fa-title", ""), ("enable-2fa-title", "Tænd for to-faktor godkendelse"),
("enable-2fa-desc", ""), ("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", ""), ("wrong-2fa-code", "Kan ikke verificere koden. Forsikr at koden og tidsindstillingerne på enheden er korrekte"),
("enter-2fa-title", ""), ("enter-2fa-title", "To-faktor godkendelse"),
("Email verification code must be 6 characters.", ""), ("Email verification code must be 6 characters.", "E-mail bekræftelseskode skal være mindst 6 tegn"),
("2FA code must be 6 digits.", ""), ("2FA code must be 6 digits.", "To-faktor kode skal være mindst 6 cifre"),
("Multiple Windows sessions found", ""), ("Multiple Windows sessions found", "Flere Windows sessioner fundet"),
("Please select the session you want to connect to", ""), ("Please select the session you want to connect to", "Vælg venligst sessionen du ønsker at forbinde til"),
("powered_by_me", ""), ("powered_by_me", "Drives af RustDesk"),
("outgoing_only_desk_tip", ""), ("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", ""), ("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", ""), ("Security Alert", "Sikkerhedsalarm"),
("My address book", ""), ("My address book", "Min adressebog"),
("Personal", ""), ("Personal", "Personlig"),
("Owner", ""), ("Owner", "Ejer"),
("Set shared password", ""), ("Set shared password", "Sæt delt adgangskode"),
("Exist in", ""), ("Exist in", "Findes i"),
("Read-only", ""), ("Read-only", "Skrivebeskyttet"),
("Read/Write", ""), ("Read/Write", "Læse/Skrive"),
("Full Control", ""), ("Full Control", "Fuld kontrol"),
("share_warning_tip", ""), ("share_warning_tip", "Felterne for oven er delt og synlige for andre."),
("Everyone", ""), ("Everyone", "Alle"),
("ab_web_console_tip", ""), ("ab_web_console_tip", "Mere på web konsollen"),
("allow-only-conn-window-open-tip", ""), ("allow-only-conn-window-open-tip", "Tillad kun fjernforbindelser hvis RustDesk vinduet er synligt"),
("no_need_privacy_mode_no_physical_displays_tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", "Ingen fysiske skærme, ingen nødvendighed for at bruge privatlivstilstanden."),
("Follow remote cursor", ""), ("Follow remote cursor", "Følg musemarkør på fjernforbindelse"),
("Follow remote window focus", ""), ("Follow remote window focus", "Følg vinduefokus på fjernforbindelse"),
("default_proxy_tip", ""), ("default_proxy_tip", "Protokollen og porten som anvendes som standard er Socks5 og 1080"),
("no_audio_input_device_tip", ""), ("no_audio_input_device_tip", "Ingen lydinput enhed fundet"),
("Incoming", ""), ("Incoming", "Indgående"),
("Outgoing", ""), ("Outgoing", "Udgående"),
("Clear Wayland screen selection", ""), ("Clear Wayland screen selection", "Ryd Wayland skærmvalg"),
("clear_Wayland_screen_selection_tip", ""), ("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", ""), ("confirm_clear_Wayland_screen_selection_tip", "Er du sikker på at du vil fjerne Wayland skærmvalget?"),
("android_new_voice_call_tip", ""), ("android_new_voice_call_tip", "Du har modtaget en ny stemmeopkaldsforespørgsel. Hvis du accepterer, vil lyden skifte til stemmekommunikation."),
("texture_render_tip", ""), ("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", ""), ("Use texture rendering", "Anvend tekstur-rendering"),
("Floating window", ""), ("Floating window", "Svævende vindue"),
("floating_window_tip", ""), ("floating_window_tip", "Det hjælper på at RustDesk baggrundstjenesten kører"),
("Keep screen on", ""), ("Keep screen on", "Hold skærmen tændt"),
("Never", ""), ("Never", "Aldrig"),
("During controlled", ""), ("During controlled", "Imens under kontrol"),
("During service is on", ""), ("During service is on", "Imens tjenesten kører"),
("Capture screen using DirectX", ""), ("Capture screen using DirectX", "Optag skærm med DirectX"),
("Back", ""), ("Back", "Tilbage"),
("Apps", ""), ("Apps", "Apps"),
("Volume up", ""), ("Volume up", "Skru op for lyd"),
("Volume down", ""), ("Volume down", "Skru ned for lyd"),
("Power", ""), ("Power", "Tænd/Sluk"),
("Telegram bot", ""), ("Telegram bot", "Telegram bot"),
("enable-bot-tip", ""), ("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", ""), ("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", ""), ("cancel-2fa-confirm-tip", "Er du sikker på at du vil afbryde to-faktor godkendelse?"),
("cancel-bot-confirm-tip", ""), ("cancel-bot-confirm-tip", "Er du sikker på at du vil afbryde Telegram robotten?"),
("About RustDesk", ""), ("About RustDesk", "Om RustDesk"),
("Send clipboard keystrokes", ""), ("Send clipboard keystrokes", "Send udklipsholder tastetryk"),
("network_error_tip", ""), ("network_error_tip", "Tjek venligst din internetforbindelse, og forsøg igen."),
("Unlock with PIN", ""), ("Unlock with PIN", "Lås op med PIN"),
("Requires at least {} characters", ""), ("Requires at least {} characters", "Kræver mindst {} tegn"),
("Wrong PIN", ""), ("Wrong PIN", "Forkert PIN"),
("Set PIN", ""), ("Set PIN", "Sæt PIN"),
("Enable trusted devices", ""), ("Enable trusted devices", "Aktivér troværdige enheder"),
("Manage trusted devices", ""), ("Manage trusted devices", "Administrér troværdige enheder"),
("Platform", ""), ("Platform", "Platform"),
("Days remaining", ""), ("Days remaining", "Dage tilbage"),
("enable-trusted-devices-tip", ""), ("enable-trusted-devices-tip", "Spring to-faktor godkendelse over på troværdige enheder"),
("Parent directory", ""), ("Parent directory", "mappe"),
("Resume", ""), ("Resume", "Fortsæt"),
("Invalid file name", ""), ("Invalid file name", "Ugyldigt filnavn"),
("one-way-file-transfer-tip", ""),
("Authentication Required", ""),
("Authenticate", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -548,7 +548,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"),
("Filter by intersection", "Nach Schnittmenge filtern"), ("Filter by intersection", "Nach Schnittmenge filtern"),
("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"), ("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."), ("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."),
("No displays", "Keine Bildschirme"), ("No displays", "Keine Bildschirme"),
("Open in new window", "In einem neuen Fenster öffnen"), ("Open in new window", "In einem neuen Fenster öffnen"),
@@ -644,5 +644,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "Übergeordnetes Verzeichnis"), ("Parent directory", "Übergeordnetes Verzeichnis"),
("Resume", "Fortsetzen"), ("Resume", "Fortsetzen"),
("Invalid file name", "Ungültiger Dateiname"), ("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"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

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