mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 22:11:30 +08:00
Compare commits
123 Commits
1.4.4
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b4f765fd | ||
|
|
370b467b71 | ||
|
|
780c396541 | ||
|
|
db5a1f29e7 | ||
|
|
b7d25ef389 | ||
|
|
626a091f55 | ||
|
|
4fa5e99e65 | ||
|
|
5ee9dcf42d | ||
|
|
6306f83316 | ||
|
|
96075fdf49 | ||
|
|
8c6dcf53a6 | ||
|
|
e1b1a927b8 | ||
|
|
1e6bfa7bb1 | ||
|
|
79ef4c4501 | ||
|
|
5f3ceef592 | ||
|
|
1a90e6b6c7 | ||
|
|
f112d097dc | ||
|
|
45cab7f808 | ||
|
|
216ec9d52b | ||
|
|
56a8f6b97b | ||
|
|
c76d10a438 | ||
|
|
f05f2178e5 | ||
|
|
226d7417b2 | ||
|
|
b0c8e65c6e | ||
|
|
4ae577c3c4 | ||
|
|
204e81a700 | ||
|
|
1f35830570 | ||
|
|
6b334f2977 | ||
|
|
0dc3c12aa5 | ||
|
|
ceffcce20e | ||
|
|
e4b06dadf5 | ||
|
|
087eb55299 | ||
|
|
341eb0c671 | ||
|
|
43b39102a4 | ||
|
|
be4bbd018d | ||
|
|
21a7cef98a | ||
|
|
a6724b1c07 | ||
|
|
7437593ee7 | ||
|
|
f21829b075 | ||
|
|
b4f60e6057 | ||
|
|
b9ebddff0c | ||
|
|
a2243484a3 | ||
|
|
c4a9835ae5 | ||
|
|
92ad279324 | ||
|
|
7276025cf9 | ||
|
|
9808d585cf | ||
|
|
dab9ed711c | ||
|
|
b27a93fc77 | ||
|
|
e3f66973b7 | ||
|
|
21529d6ca2 | ||
|
|
775b0a3c93 | ||
|
|
070d4d029f | ||
|
|
5355702e9c | ||
|
|
a97997952d | ||
|
|
b0c12bd86b | ||
|
|
82fcab26b1 | ||
|
|
f3bbcc4f55 | ||
|
|
98362eaca0 | ||
|
|
998b75856d | ||
|
|
3a9084006f | ||
|
|
4d3ccc62e8 | ||
|
|
8fe10d61ea | ||
|
|
5a183490dc | ||
|
|
9dd4fa8646 | ||
|
|
a05b619563 | ||
|
|
7f9506b476 | ||
|
|
f65952cf1c | ||
|
|
7ac03ffefc | ||
|
|
f6d6c3afb5 | ||
|
|
419703d2ea | ||
|
|
9301edef06 | ||
|
|
7e3f0a607b | ||
|
|
dec0e7c56d | ||
|
|
0758e10ae2 | ||
|
|
19ae785fa2 | ||
|
|
918ce865ca | ||
|
|
d27a21feee | ||
|
|
d8932b69a3 | ||
|
|
5af580f44d | ||
|
|
3384eda8b7 | ||
|
|
969ea28d06 | ||
|
|
5b2101e17d | ||
|
|
ec2d7f0519 | ||
|
|
656ce93d6e | ||
|
|
b69e871f9a | ||
|
|
bba57069a8 | ||
|
|
6a701f1420 | ||
|
|
eba847e62e | ||
|
|
b80eb2dc6c | ||
|
|
1f9689dc00 | ||
|
|
84eb75d5b6 | ||
|
|
4f2aea65ab | ||
|
|
d6463f95b9 | ||
|
|
3e0688ab63 | ||
|
|
692e90f779 | ||
|
|
e4faedcb62 | ||
|
|
a32d36a97b | ||
|
|
da2c678fb3 | ||
|
|
7bdfa121f3 | ||
|
|
b9a1369c6f | ||
|
|
0112b3387e | ||
|
|
de9d86621d | ||
|
|
735862d1fd | ||
|
|
a0537759b1 | ||
|
|
a79776c1c4 | ||
|
|
822b6d1baf | ||
|
|
0065085ba2 | ||
|
|
4f4da20fc0 | ||
|
|
eb0174ea53 | ||
|
|
20ce626654 | ||
|
|
a342941ec1 | ||
|
|
a78a803a22 | ||
|
|
23754630e8 | ||
|
|
8e6e91eb4a | ||
|
|
9cfa551163 | ||
|
|
5b21441898 | ||
|
|
4ed8696d1d | ||
|
|
ae06f27372 | ||
|
|
33e1493932 | ||
|
|
22b1dcaf7b | ||
|
|
426a68775f | ||
|
|
3c0be4e40e | ||
|
|
3787b45b49 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -84,6 +84,20 @@ jobs:
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
|
||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
if: runner.os == 'Linux'
|
||||
# jlumbroso/free-disk-space@main is used in .github\workflows\flutter-build.yml
|
||||
# But pinning to a specific version to avoid unexpected issues is preferred.
|
||||
uses: jlumbroso/free-disk-space@v1.3.1
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: false
|
||||
docker-images: true
|
||||
swap-storage: false
|
||||
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
|
||||
37
.github/workflows/flutter-build.yml
vendored
37
.github/workflows/flutter-build.yml
vendored
@@ -39,13 +39,13 @@ env:
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.4"
|
||||
VERSION: "1.4.5"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2"
|
||||
|
||||
jobs:
|
||||
generate-bridge:
|
||||
@@ -234,11 +234,11 @@ jobs:
|
||||
path: rustdesk
|
||||
|
||||
- name: Sign rustdesk files
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
|
||||
shell: bash
|
||||
run: |
|
||||
pip3 install requests argparse
|
||||
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/
|
||||
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/
|
||||
|
||||
- name: Build self-extracted executable
|
||||
shell: bash
|
||||
@@ -266,10 +266,10 @@ jobs:
|
||||
sha256sum ../../SignOutput/rustdesk-*.msi
|
||||
|
||||
- name: Sign rustdesk self-extracted file
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
|
||||
shell: bash
|
||||
run: |
|
||||
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
|
||||
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
|
||||
|
||||
- name: Publish Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -400,11 +400,11 @@ jobs:
|
||||
path: Release
|
||||
|
||||
- name: Sign rustdesk files
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
|
||||
shell: bash
|
||||
run: |
|
||||
pip3 install requests argparse
|
||||
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/
|
||||
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/
|
||||
|
||||
- name: Build self-extracted executable
|
||||
shell: bash
|
||||
@@ -418,10 +418,10 @@ jobs:
|
||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
|
||||
|
||||
- name: Sign rustdesk self-extracted file
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != ''
|
||||
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
|
||||
shell: bash
|
||||
run: |
|
||||
BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
|
||||
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
|
||||
|
||||
- name: Publish Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -444,7 +444,7 @@ jobs:
|
||||
- {
|
||||
arch: aarch64,
|
||||
target: aarch64-apple-ios,
|
||||
os: macos-13,
|
||||
os: macos-latest,
|
||||
vcpkg-triplet: arm64-ios,
|
||||
}
|
||||
steps:
|
||||
@@ -562,7 +562,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
|
||||
extra-build-args: "",
|
||||
arch: x86_64,
|
||||
vcpkg-triplet: x64-osx,
|
||||
@@ -623,7 +623,7 @@ jobs:
|
||||
|
||||
- name: Install build runtime
|
||||
run: |
|
||||
brew install llvm create-dmg nasm
|
||||
brew install llvm create-dmg
|
||||
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
|
||||
if command -v pkg-config &>/dev/null; then
|
||||
echo "pkg-config is already installed"
|
||||
@@ -631,6 +631,17 @@ jobs:
|
||||
brew install pkg-config
|
||||
fi
|
||||
|
||||
- name: Install NASM
|
||||
run: |
|
||||
# Install NASM 2.16.x from official release.
|
||||
# Do NOT use `brew install nasm` which installs NASM 3.x.
|
||||
# NASM 3.x is a complete rewrite with incompatible CLI options and removed features.
|
||||
# aom and other multimedia libraries require NASM 2.x for x86/x86_64 assembly.
|
||||
wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/macosx/nasm-2.16.03-macosx.zip
|
||||
unzip nasm-2.16.03-macosx.zip
|
||||
sudo cp nasm-2.16.03/nasm /usr/local/bin/nasm
|
||||
nasm --version
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.4"
|
||||
VERSION: "1.4.5"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: "1.4.4"
|
||||
release-tag: "1.4.4"
|
||||
version: "1.4.5"
|
||||
release-tag: "1.4.5"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
1036
Cargo.lock
generated
1036
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.4"
|
||||
version = "1.4.5"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -76,7 +76,6 @@ crossbeam-queue = "0.3"
|
||||
hex = "0.4"
|
||||
chrono = "0.4"
|
||||
cidr-utils = "0.5"
|
||||
libloading = "0.8"
|
||||
fon = "0.6"
|
||||
zip = "0.6"
|
||||
shutdown_hooks = "0.1"
|
||||
@@ -123,10 +122,19 @@ winapi = { version = "0.3", features = [
|
||||
] }
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System",
|
||||
"Win32_System_Diagnostics",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Environment",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
@@ -168,6 +176,7 @@ bytemuck = "1.23"
|
||||
ttf-parser = "0.25"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libxdo-sys = "0.11"
|
||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||
pulse = { package = "libpulse-binding", version = "2.27" }
|
||||
rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" }
|
||||
@@ -176,7 +185,6 @@ evdev = { git="https://github.com/rustdesk-org/evdev" }
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
pam = { git="https://github.com/rustdesk-org/pam" }
|
||||
users = { version = "0.11" }
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
percent-encoding = {version = "2.3", optional = true}
|
||||
@@ -199,6 +207,11 @@ android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" }
|
||||
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"]
|
||||
exclude = ["vdi/host", "examples/custom_plugin"]
|
||||
|
||||
# Patch libxdo-sys to use a stub implementation that doesn't require libxdo
|
||||
# This allows building and running on systems without libxdo installed (e.g., Wayland-only)
|
||||
[patch.crates-io]
|
||||
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.4
|
||||
version: 1.4.5
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.4
|
||||
version: 1.4.5
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
2
build.rs
2
build.rs
@@ -18,7 +18,7 @@ fn build_mac() {
|
||||
b.flag("-DNO_InputMonitoringAuthStatus=1");
|
||||
}
|
||||
}
|
||||
b.file(file).compile("macos");
|
||||
b.flag("-std=c++17").file(file).compile("macos");
|
||||
println!("cargo:rerun-if-changed={}", file);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<p align="center">
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#freie-öffentliche-server">Server</a> •
|
||||
<img src="../res/logo-header.svg" alt="RustDesk - Dein Remote-Desktop"><br>
|
||||
<a href="#grobe-schritte-zum-kompilieren">Kompilieren</a> •
|
||||
<a href="#auf-docker-kompilieren">Docker</a> •
|
||||
<a href="#dateistruktur">Dateistruktur</a> •
|
||||
<a href="#screenshots">Screenshots</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</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-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-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>] | [<a href="docs/README-RO.md">Română</a>]<br>
|
||||
<b>Wir brauchen Ihre Hilfe, um dieses README, die <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk-Benutzeroberfläche</a> und die <a href="https://github.com/rustdesk/doc.rustdesk.com">Dokumentation</a> in Ihre Muttersprache zu übersetzen.</b>
|
||||
</p>
|
||||
|
||||
> [!Vorsicht]
|
||||
> [!Caution]
|
||||
> **Haftungsausschluss bei Missbrauch::** <br>
|
||||
> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung.
|
||||
|
||||
@@ -28,11 +27,14 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE
|
||||
|
||||
[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
[**Nightly Builds**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||
[<img src="https://flathub.org/api/badge?svg&locale=en"
|
||||
alt="Get it on Flathub"
|
||||
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
@@ -64,18 +66,19 @@ Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter.
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
@@ -114,7 +117,7 @@ cd
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
@@ -129,6 +132,7 @@ Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen:
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
@@ -157,6 +161,7 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Datei kopieren und einfügen Implementierung für Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung
|
||||
@@ -167,10 +172,11 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -13,7 +13,9 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](http
|
||||
|
||||
[](https://rustdesk.com/pricing.html)
|
||||
|
||||
Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
## O projekcie
|
||||
|
||||
RustDesk to wieloplatformowe oprogramowanie do zdalnego pulpitu, napisane w języku Rust, zaprojektowane z myślą o prostocie wdrożenia, bezpieczeństwie i pełnej kontroli użytkownika nad danymi. Aplikacja działa od razu po uruchomieniu i nie wymaga skomplikowanej konfiguracji. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
@@ -31,7 +33,7 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C
|
||||
|
||||
## Zależności
|
||||
|
||||
Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter.
|
||||
Wersje desktopowe korzystają z biblioteki [sciter](https://sciter.com/) jako silnika GUI. Bibliotekę Sciter należy pobrać i zainstalować samodzielnie.
|
||||
|
||||
[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) |
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" style="isolation:isolate" viewBox="541.937 521.772 32 32"><path fill="none" d="M541.937 521.772h32v32h-32v-32Z"/><path fill-rule="evenodd" d="M552.145 539.981h11.584c.446 0 .808.362.808.808v.536c0 .786-.639 1.425-1.425 1.425h-10.35a1.426 1.426 0 0 1-1.425-1.425v-.536c0-.446.362-.808.808-.808Zm-1.761-3.511h.899c.536 0 .971.435.971.971v.899a.971.971 0 0 1-.971.971h-.899a.971.971 0 0 1-.971-.971v-.899c0-.536.435-.971.971-.971Zm3.552 0h.899c.536 0 .971.435.971.971v.899a.971.971 0 0 1-.971.971h-.899a.972.972 0 0 1-.972-.971v-.899c0-.536.436-.971.972-.971Zm3.551 0h.9c.536 0 .971.435.971.971v.899a.971.971 0 0 1-.971.971h-.9a.971.971 0 0 1-.971-.971v-.899c0-.536.435-.971.971-.971Zm3.552 0h.899c.536 0 .972.435.972.971v.899a.972.972 0 0 1-.972.971h-.899a.971.971 0 0 1-.971-.971v-.899c0-.536.435-.971.971-.971Zm3.552 0h.899c.536 0 .971.435.971.971v.899a.971.971 0 0 1-.971.971h-.899a.971.971 0 0 1-.971-.971v-.899c0-.536.435-.971.971-.971Zm-14.383-3.512h1.25c.44 0 .796.357.796.796v1.25a.796.796 0 0 1-.796.796h-1.25a.796.796 0 0 1-.795-.796v-1.25c0-.439.356-.796.795-.796Zm3.552 0h1.25c.439 0 .796.357.796.796v1.25a.797.797 0 0 1-.796.796h-1.25a.797.797 0 0 1-.796-.796v-1.25c0-.439.357-.796.796-.796Zm3.552 0h1.25c.439 0 .796.357.796.796v1.25a.797.797 0 0 1-.796.796h-1.25a.797.797 0 0 1-.796-.796v-1.25c0-.439.357-.796.796-.796Zm3.552 0h1.25c.439 0 .796.357.796.796v1.25a.797.797 0 0 1-.796.796h-1.25a.797.797 0 0 1-.796-.796v-1.25c0-.439.357-.796.796-.796Zm-9.553-3.85h13.252c1.407 0 2.755.507 3.748 1.409.993.902 1.552 2.127 1.552 3.404v7.702c0 1.277-.559 2.501-1.552 3.403-.993.902-2.341 1.409-3.748 1.409h-13.252c-1.407 0-2.755-.507-3.748-1.409-.993-.902-1.552-2.126-1.552-3.403v-7.702c0-1.277.559-2.502 1.552-3.404.993-.902 2.341-1.409 3.748-1.409Zm13.105 3.85h1.25c.439 0 .795.357.795.796v1.25a.796.796 0 0 1-.795.796h-1.25a.796.796 0 0 1-.796-.796v-1.25c0-.439.356-.796.796-.796Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
1
flutter/assets/keyboard_mouse.svg
Normal file
1
flutter/assets/keyboard_mouse.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.4 KiB |
@@ -24,6 +24,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:window_size/window_size.dart' as window_size;
|
||||
|
||||
@@ -1010,13 +1011,15 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||
});
|
||||
}
|
||||
|
||||
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
||||
void showToast(String text,
|
||||
{Duration timeout = const Duration(seconds: 3),
|
||||
Alignment alignment = const Alignment(0.0, 0.8)}) {
|
||||
final overlayState = globalKey.currentState?.overlay;
|
||||
if (overlayState == null) return;
|
||||
final entry = OverlayEntry(builder: (context) {
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
alignment: const Alignment(0.0, 0.8),
|
||||
alignment: alignment,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).toastBg,
|
||||
@@ -1121,18 +1124,23 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
|
||||
Widget createDialogContent(String text) {
|
||||
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||
bool hasLink = linkRegExp.hasMatch(text);
|
||||
|
||||
// Early return: no link, use default theme color
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
final List<TextSpan> spans = [];
|
||||
int start = 0;
|
||||
bool hasLink = false;
|
||||
|
||||
linkRegExp.allMatches(text).forEach((match) {
|
||||
hasLink = true;
|
||||
if (match.start > start) {
|
||||
spans.add(TextSpan(text: text.substring(start, match.start)));
|
||||
}
|
||||
spans.add(TextSpan(
|
||||
text: match.group(0) ?? '',
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -1150,13 +1158,9 @@ Widget createDialogContent(String text) {
|
||||
spans.add(TextSpan(text: text.substring(start)));
|
||||
}
|
||||
|
||||
if (!hasLink) {
|
||||
return SelectableText(text, style: const TextStyle(fontSize: 15));
|
||||
}
|
||||
|
||||
return SelectableText.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(color: Colors.black, fontSize: 15),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
children: spans,
|
||||
),
|
||||
);
|
||||
@@ -1575,7 +1579,7 @@ bool option2bool(String option, String value) {
|
||||
option == kOptionForceAlwaysRelay) {
|
||||
res = value == "Y";
|
||||
} else {
|
||||
assert(false);
|
||||
// "" is true
|
||||
res = value != "N";
|
||||
}
|
||||
return res;
|
||||
@@ -1593,9 +1597,6 @@ String bool2option(String option, bool b) {
|
||||
option == kOptionForceAlwaysRelay) {
|
||||
res = b ? 'Y' : defaultOptionNo;
|
||||
} else {
|
||||
if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) {
|
||||
assert(false);
|
||||
}
|
||||
res = b ? 'Y' : 'N';
|
||||
}
|
||||
return res;
|
||||
@@ -1932,44 +1933,41 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
||||
return null;
|
||||
}
|
||||
|
||||
double? frameLeft;
|
||||
double? frameTop;
|
||||
double? frameRight;
|
||||
double? frameBottom;
|
||||
|
||||
if (isDesktop || isWebDesktop) {
|
||||
for (final screen in await window_size.getScreenList()) {
|
||||
frameLeft = frameLeft == null
|
||||
? screen.visibleFrame.left
|
||||
: min(screen.visibleFrame.left, frameLeft);
|
||||
frameTop = frameTop == null
|
||||
? screen.visibleFrame.top
|
||||
: min(screen.visibleFrame.top, frameTop);
|
||||
frameRight = frameRight == null
|
||||
? screen.visibleFrame.right
|
||||
: max(screen.visibleFrame.right, frameRight);
|
||||
frameBottom = frameBottom == null
|
||||
? screen.visibleFrame.bottom
|
||||
: max(screen.visibleFrame.bottom, frameBottom);
|
||||
final screens = await window_size.getScreenList();
|
||||
if (screens.isNotEmpty) {
|
||||
final windowRect = Rect.fromLTWH(left, top, width, height);
|
||||
bool isVisible = false;
|
||||
for (final screen in screens) {
|
||||
final intersection = windowRect.intersect(screen.visibleFrame);
|
||||
if (intersection.width >= 10.0 && intersection.height >= 10.0) {
|
||||
isVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
return Offset(left, top);
|
||||
}
|
||||
}
|
||||
if (frameLeft == null) {
|
||||
frameLeft = 0.0;
|
||||
frameTop = 0.0;
|
||||
frameRight = ((isDesktop || isWebDesktop)
|
||||
? kDesktopMaxDisplaySize
|
||||
: kMobileMaxDisplaySize)
|
||||
.toDouble();
|
||||
frameBottom = ((isDesktop || isWebDesktop)
|
||||
? kDesktopMaxDisplaySize
|
||||
: kMobileMaxDisplaySize)
|
||||
.toDouble();
|
||||
}
|
||||
|
||||
double frameLeft = 0.0;
|
||||
double frameTop = 0.0;
|
||||
double frameRight = ((isDesktop || isWebDesktop)
|
||||
? kDesktopMaxDisplaySize
|
||||
: kMobileMaxDisplaySize)
|
||||
.toDouble();
|
||||
double frameBottom = ((isDesktop || isWebDesktop)
|
||||
? kDesktopMaxDisplaySize
|
||||
: kMobileMaxDisplaySize)
|
||||
.toDouble();
|
||||
|
||||
final minWidth = 10.0;
|
||||
if ((left + minWidth) > frameRight! ||
|
||||
(top + minWidth) > frameBottom! ||
|
||||
if ((left + minWidth) > frameRight ||
|
||||
(top + minWidth) > frameBottom ||
|
||||
(left + width - minWidth) < frameLeft ||
|
||||
top < frameTop!) {
|
||||
top < frameTop) {
|
||||
return null;
|
||||
} else {
|
||||
return Offset(left, top);
|
||||
@@ -2676,6 +2674,55 @@ class SimpleWrapper<T> {
|
||||
SimpleWrapper(this.value);
|
||||
}
|
||||
|
||||
/// Wakelock manager with reference counting for desktop.
|
||||
/// Ensures wakelock is only disabled when all sessions are closed/minimized.
|
||||
///
|
||||
/// Note: Each isolate has its own WakelockPlus instance with independent assertion.
|
||||
/// As long as one isolate has wakelock enabled, the screen stays awake.
|
||||
/// This manager handles multiple tabs within the same isolate.
|
||||
class WakelockManager {
|
||||
static final Set<UniqueKey> _enabledKeys = {};
|
||||
// Don't use WakelockPlus.enabled, it causes error on Android:
|
||||
// Unhandled Exception: FormatException: Message corrupted
|
||||
//
|
||||
// On Linux, multiple enable() calls create only one inhibit, but each disable()
|
||||
// only releases if _cookie != null. So we need our own _enabled state to avoid
|
||||
// calling disable() when not enabled.
|
||||
// See: https://github.com/fluttercommunity/wakelock_plus/blob/0c74e5bbc6aefac57b6c96bb7ef987705ed559ec/wakelock_plus/lib/src/wakelock_plus_linux_plugin.dart#L48
|
||||
static bool _enabled = false;
|
||||
|
||||
static void enable(UniqueKey key, {bool isServer = false}) {
|
||||
// Check if we should keep awake during outgoing sessions
|
||||
if (!isServer) {
|
||||
final keepAwake =
|
||||
mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions);
|
||||
if (!keepAwake) {
|
||||
return; // Don't enable wakelock if user disabled keep awake
|
||||
}
|
||||
}
|
||||
if (isDesktop) {
|
||||
_enabledKeys.add(key);
|
||||
}
|
||||
if (!_enabled) {
|
||||
_enabled = true;
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
}
|
||||
|
||||
static void disable(UniqueKey key) {
|
||||
if (isDesktop) {
|
||||
_enabledKeys.remove(key);
|
||||
if (_enabledKeys.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_enabled) {
|
||||
WakelockPlus.disable();
|
||||
_enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// call this to reload current window.
|
||||
///
|
||||
/// [Note]
|
||||
@@ -3016,10 +3063,21 @@ Future<void> start_service(bool is_start) async {
|
||||
}
|
||||
|
||||
Future<bool> canBeBlocked() async {
|
||||
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||
// First check control permission
|
||||
final controlPermission = await bind.mainGetCommon(
|
||||
key: "is-remote-modify-enabled-by-control-permissions");
|
||||
if (controlPermission == "true") {
|
||||
return false;
|
||||
} else if (controlPermission == "false") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check local settings
|
||||
var accessMode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||
var isCustomAccessMode = accessMode != 'full' && accessMode != 'view';
|
||||
var option = option2bool(kOptionAllowRemoteConfigModification,
|
||||
await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
|
||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||
return accessMode == 'view' || (isCustomAccessMode && !option);
|
||||
}
|
||||
|
||||
// to-do: web not implemented
|
||||
@@ -3782,6 +3840,16 @@ setResizable(bool resizable) {
|
||||
|
||||
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||
|
||||
bool isChangePermanentPasswordDisabled() =>
|
||||
bind.mainGetBuildinOption(key: kOptionDisableChangePermanentPassword) ==
|
||||
'Y';
|
||||
|
||||
bool isChangeIdDisabled() =>
|
||||
bind.mainGetBuildinOption(key: kOptionDisableChangeId) == 'Y';
|
||||
|
||||
bool isUnlockPinDisabled() =>
|
||||
bind.mainGetBuildinOption(key: kOptionDisableUnlockPin) == 'Y';
|
||||
|
||||
bool? _isCustomClient;
|
||||
bool get isCustomClient {
|
||||
_isCustomClient ??= bind.isCustomClient();
|
||||
@@ -4025,3 +4093,23 @@ String decode_http_response(http.Response resp) {
|
||||
bool peerTabShowNote(PeerTabIndex peerTabIndex) {
|
||||
return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group;
|
||||
}
|
||||
|
||||
// TODO: We should support individual bits combinations in the future.
|
||||
// But for now, just keep it simple, because the old code only supports single button.
|
||||
// No users have requested multi-button support yet.
|
||||
String mouseButtonsToPeer(int buttons) {
|
||||
switch (buttons) {
|
||||
case kPrimaryMouseButton:
|
||||
return 'left';
|
||||
case kSecondaryMouseButton:
|
||||
return 'right';
|
||||
case kMiddleMouseButton:
|
||||
return 'wheel';
|
||||
case kBackMouseButton:
|
||||
return 'back';
|
||||
case kForwardMouseButton:
|
||||
return 'forward';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
GestureDragStartCallback? onOneFingerPanStart;
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate;
|
||||
GestureDragEndCallback? onOneFingerPanEnd;
|
||||
GestureDragCancelCallback? onOneFingerPanCancel;
|
||||
|
||||
// twoFingerScale : scale + pan event
|
||||
GestureScaleStartCallback? onTwoFingerScaleStart;
|
||||
@@ -169,6 +170,27 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
|
||||
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
|
||||
DragEndDetails(velocity: d.velocity);
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
if (onOneFingerPanCancel != null) {
|
||||
onOneFingerPanCancel!();
|
||||
}
|
||||
break;
|
||||
case GestureState.twoFingerScale:
|
||||
// Reset scale state if needed, currently self-contained
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
// Reset drag state if needed, currently self-contained
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_currentState = GestureState.none;
|
||||
}
|
||||
}
|
||||
|
||||
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
|
||||
@@ -717,6 +739,7 @@ RawGestureDetector getMixinGestureDetector({
|
||||
GestureDragStartCallback? onOneFingerPanStart,
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate,
|
||||
GestureDragEndCallback? onOneFingerPanEnd,
|
||||
GestureDragCancelCallback? onOneFingerPanCancel,
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd,
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
|
||||
@@ -765,6 +788,7 @@ RawGestureDetector getMixinGestureDetector({
|
||||
..onOneFingerPanStart = onOneFingerPanStart
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
||||
|
||||
@@ -103,7 +103,7 @@ class ButtonOP extends StatelessWidget {
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
child: Text('${translate("Continue with")} $opLabel')),
|
||||
child: Text(translate("Continue with {$opLabel}"))),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
// For mouse mode, we need to block the events when the cursor is in a blocked area.
|
||||
// So we need to cache the last tap down position.
|
||||
Offset? _lastTapDownPositionForMouseMode;
|
||||
// Cache global position for onTap (which lacks position info).
|
||||
Offset? _lastTapDownGlobalPosition;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
@@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
|
||||
onTapDown(TapDownDetails d) async {
|
||||
lastDeviceKind = d.kind;
|
||||
_lastTapDownGlobalPosition = d.globalPosition;
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
@@ -154,11 +157,16 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
|
||||
if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
final isMoved =
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
if (isMoved) {
|
||||
if (lastTapDownDetails != null) {
|
||||
// If pan already handled 'down', don't send it again.
|
||||
if (lastTapDownDetails != null && !_touchModePanStarted) {
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
@@ -170,6 +178,11 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (isNotTouchBasedDevice()) {
|
||||
return;
|
||||
}
|
||||
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
|
||||
final lastPos = _lastTapDownGlobalPosition;
|
||||
if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
|
||||
// Using `_lastTapDownPositionForMouseMode` instead.
|
||||
@@ -372,7 +385,10 @@ class _RawTouchGestureDetectorRegionState
|
||||
await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
}
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
// In relative mouse mode, skip mouse down - only send movement via sendMobileRelativeMouseMove
|
||||
if (!inputModel.relativeMouseMode.value) {
|
||||
await inputModel.sendMouse('down', MouseButtons.left);
|
||||
}
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
} else {
|
||||
final offset = ffi.cursorModel.offset;
|
||||
@@ -397,7 +413,12 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (handleTouch && !_touchModePanStarted) {
|
||||
return;
|
||||
}
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
// In relative mouse mode, send delta directly without position tracking.
|
||||
if (inputModel.relativeMouseMode.value) {
|
||||
await inputModel.sendMobileRelativeMouseMove(d.delta.dx, d.delta.dy);
|
||||
} else {
|
||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) async {
|
||||
@@ -409,10 +430,21 @@ class _RawTouchGestureDetectorRegionState
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
if (handleTouch) {
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
// In relative mouse mode, skip mouse up - matches the skipped mouse down in onOneFingerPanStart
|
||||
if (!inputModel.relativeMouseMode.value) {
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled
|
||||
// or rejected by the gesture arena. Without this, the flag can remain
|
||||
// stuck in the "started" state and cause issues such as the Magic Mouse
|
||||
// double-click problem on iPad with magic mouse.
|
||||
onOneFingerPanCancel() {
|
||||
_touchModePanStarted = false;
|
||||
}
|
||||
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
@@ -546,6 +578,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
instance
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleStart = onTwoFingerScaleStart
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
|
||||
@@ -6,10 +6,12 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
@@ -193,14 +195,26 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
);
|
||||
}
|
||||
// note
|
||||
if (isDefaultConn &&
|
||||
bind
|
||||
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn")
|
||||
.isNotEmpty) {
|
||||
if (isDefaultConn && !bind.isDisableAccount()) {
|
||||
v.add(
|
||||
TTextMenu(
|
||||
child: Text(translate('Note')),
|
||||
onPressed: () => showAuditDialog(ffi)),
|
||||
onPressed: () async {
|
||||
bool isLogin =
|
||||
bind.mainGetLocalOption(key: 'access_token').isNotEmpty;
|
||||
if (!isLogin) {
|
||||
final res = await loginDialog();
|
||||
if (res != true) return;
|
||||
// Desktop: send message to main window to refresh login status
|
||||
// Web: login is required before connection, so no need to refresh
|
||||
// Mobile: same isolate, no need to send message
|
||||
if (isDesktop) {
|
||||
rustDeskWinManager.call(
|
||||
WindowType.Main, kWindowRefreshCurrentUser, "");
|
||||
}
|
||||
}
|
||||
showAuditDialog(ffi);
|
||||
}),
|
||||
);
|
||||
}
|
||||
// divider
|
||||
@@ -817,6 +831,7 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final sessionId = ffi.sessionId;
|
||||
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||
List<TToggleMenu> v = [];
|
||||
|
||||
// swap key
|
||||
@@ -838,6 +853,34 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
||||
child: Text(translate('Swap control-command key'))));
|
||||
}
|
||||
|
||||
// Relative mouse mode (gaming mode).
|
||||
// Only show when server supports MOUSE_TYPE_MOVE_RELATIVE (version >= 1.4.5)
|
||||
// Note: This feature is only available in Flutter client. Sciter client does not support this.
|
||||
// Web client is not supported yet due to Pointer Lock API integration complexity with Flutter's input system.
|
||||
// Wayland is not supported due to cursor warping limitations.
|
||||
// Mobile: This option is now in GestureHelp widget, shown only when joystick is visible.
|
||||
final isWayland = isDesktop && isLinux && bind.mainCurrentIsWayland();
|
||||
if (isDesktop &&
|
||||
isDefaultConn &&
|
||||
!isWeb &&
|
||||
!isWayland &&
|
||||
ffiModel.keyboard &&
|
||||
!ffiModel.viewOnly &&
|
||||
ffi.inputModel.isRelativeMouseModeSupported) {
|
||||
v.add(TToggleMenu(
|
||||
value: ffi.inputModel.relativeMouseMode.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
final previousValue = ffi.inputModel.relativeMouseMode.value;
|
||||
final success = ffi.inputModel.setRelativeMouseMode(value);
|
||||
if (!success) {
|
||||
// Revert the observable toggle to reflect the actual state
|
||||
ffi.inputModel.relativeMouseMode.value = previousValue;
|
||||
}
|
||||
},
|
||||
child: Text(translate('Relative mouse mode'))));
|
||||
}
|
||||
|
||||
// reverse mouse wheel
|
||||
if (ffiModel.keyboard) {
|
||||
var optionValue =
|
||||
|
||||
@@ -50,6 +50,7 @@ const String kAppTypeDesktopPortForward = "port forward";
|
||||
const String kAppTypeDesktopTerminal = "terminal";
|
||||
|
||||
const String kWindowMainWindowOnTop = "main_window_on_top";
|
||||
const String kWindowRefreshCurrentUser = "refresh_current_user";
|
||||
const String kWindowGetWindowInfo = "get_window_info";
|
||||
const String kWindowGetScreenList = "get_screen_list";
|
||||
// This method is not used, maybe it can be removed.
|
||||
@@ -120,6 +121,7 @@ const String kOptionApproveMode = "approve-mode";
|
||||
const String kOptionAllowNumericOneTimePassword =
|
||||
"allow-numeric-one-time-password";
|
||||
const String kOptionCollapseToolbar = "collapse_toolbar";
|
||||
const String kOptionHideToolbar = "hide-toolbar";
|
||||
const String kOptionShowRemoteCursor = "show_remote_cursor";
|
||||
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
|
||||
const String kOptionFollowRemoteWindow = "follow_remote_window";
|
||||
@@ -161,6 +163,7 @@ const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
|
||||
|
||||
// network options
|
||||
const String kOptionAllowWebSocket = "allow-websocket";
|
||||
@@ -177,6 +180,10 @@ const String kOptionHideSecuritySetting = "hide-security-settings";
|
||||
const String kOptionHideNetworkSetting = "hide-network-settings";
|
||||
const String kOptionRemovePresetPasswordWarning =
|
||||
"remove-preset-password-warning";
|
||||
const String kOptionDisableChangePermanentPassword =
|
||||
"disable-change-permanent-password";
|
||||
const String kOptionDisableChangeId = "disable-change-id";
|
||||
const String kOptionDisableUnlockPin = "disable-unlock-pin";
|
||||
const kHideUsernameOnCard = "hide-username-on-card";
|
||||
const String kOptionHideHelpCards = "hide-help-cards";
|
||||
|
||||
@@ -187,6 +194,9 @@ const String kOptionDisableFloatingWindow = "disable-floating-window";
|
||||
|
||||
const String kOptionKeepScreenOn = "keep-screen-on";
|
||||
|
||||
const String kOptionKeepAwakeDuringIncomingSessions = "keep-awake-during-incoming-sessions";
|
||||
const String kOptionKeepAwakeDuringOutgoingSessions = "keep-awake-during-outgoing-sessions";
|
||||
|
||||
const String kOptionShowMobileAction = "showMobileActions";
|
||||
|
||||
const String kUrlActionClose = "close";
|
||||
@@ -251,6 +261,33 @@ const int kMinTrackpadSpeed = 10;
|
||||
const int kDefaultTrackpadSpeed = 100;
|
||||
const int kMaxTrackpadSpeed = 1000;
|
||||
|
||||
// relative mouse mode
|
||||
/// Throttle duration (in milliseconds) for updating pointer lock center during
|
||||
/// window move/resize events. Lower values provide more responsive updates but
|
||||
/// may cause performance issues during rapid window operations.
|
||||
const int kDefaultPointerLockCenterThrottleMs = 100;
|
||||
|
||||
/// Minimum server version required for relative mouse mode (MOUSE_TYPE_MOVE_RELATIVE).
|
||||
/// Servers older than this version will ignore relative mouse events.
|
||||
///
|
||||
/// IMPORTANT: This value must be kept in sync with the Rust constant
|
||||
/// `MIN_VERSION_RELATIVE_MOUSE_MODE` in `src/common.rs`.
|
||||
const String kMinVersionForRelativeMouseMode = '1.4.5';
|
||||
|
||||
/// Maximum delta value for relative mouse movement.
|
||||
/// Large values could cause issues with i32 overflow on server side,
|
||||
/// and no reasonable mouse movement should exceed this bound.
|
||||
///
|
||||
/// IMPORTANT: This value must be kept in sync with the Rust constant
|
||||
/// `MAX_RELATIVE_MOUSE_DELTA` in `src/server/input_service.rs`.
|
||||
const int kMaxRelativeMouseDelta = 10000;
|
||||
|
||||
/// Debounce duration (in milliseconds) for relative mouse mode toggle.
|
||||
/// This prevents double-toggle from race condition between Rust rdev grab loop
|
||||
/// and Flutter keyboard handling. Value should be small enough to allow
|
||||
/// intentional quick toggles but large enough to prevent accidental double-triggers.
|
||||
const int kRelativeMouseModeToggleDebounceMs = 150;
|
||||
|
||||
// incomming (should be incoming) is kept, because change it will break the previous setting.
|
||||
const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action';
|
||||
const String kValuePrinterIncomingJobDismiss = 'dismiss';
|
||||
|
||||
@@ -450,7 +450,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
"${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
|
||||
btnText,
|
||||
onPressed,
|
||||
closeButton: true);
|
||||
closeButton: true,
|
||||
help: isToUpdate ? 'Changelog' : null,
|
||||
link: isToUpdate
|
||||
? 'https://github.com/rustdesk/rustdesk/releases/tag/${bind.mainGetNewVersion()}'
|
||||
: null);
|
||||
}
|
||||
if (systemError.isNotEmpty) {
|
||||
return buildInstallCard("", systemError, "", () {});
|
||||
@@ -776,6 +780,8 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
}
|
||||
if (call.method == kWindowMainWindowOnTop) {
|
||||
windowOnTop(null);
|
||||
} else if (call.method == kWindowRefreshCurrentUser) {
|
||||
gFFI.userModel.refreshCurrentUser();
|
||||
} else if (call.method == kWindowGetWindowInfo) {
|
||||
final screen = (await window_size.getWindowInfo()).screen;
|
||||
if (screen == null) {
|
||||
|
||||
@@ -557,16 +557,36 @@ class _GeneralState extends State<_General> {
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Add client-side wakelock option for desktop platforms
|
||||
if (!bind.isIncomingOnly()) {
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'keep-awake-during-outgoing-sessions-label',
|
||||
kOptionKeepAwakeDuringOutgoingSessions,
|
||||
isServer: false,
|
||||
));
|
||||
}
|
||||
|
||||
if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
||||
children.add(_OptionCheckBox(
|
||||
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
||||
}
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'note-at-conn-end-tip',
|
||||
kOptionAllowAskForNoteAtEndOfConnection,
|
||||
isServer: false,
|
||||
));
|
||||
if (!bind.isDisableAccount()) {
|
||||
children.add(_OptionCheckBox(
|
||||
context,
|
||||
'note-at-conn-end-tip',
|
||||
kOptionAllowAskForNoteAtEndOfConnection,
|
||||
isServer: false,
|
||||
optSetter: (key, value) async {
|
||||
if (value && !gFFI.userModel.isLogin) {
|
||||
final res = await loginDialog();
|
||||
if (res != true) return;
|
||||
}
|
||||
await mainSetLocalBoolOption(key, value);
|
||||
},
|
||||
));
|
||||
}
|
||||
return _Card(title: 'Other', children: children);
|
||||
}
|
||||
|
||||
@@ -816,7 +836,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
permissions(context),
|
||||
password(context),
|
||||
_Card(title: '2FA', children: [tfa()]),
|
||||
_Card(title: 'ID', children: [changeId()]),
|
||||
if (!isChangeIdDisabled())
|
||||
_Card(title: 'ID', children: [changeId()]),
|
||||
more(context),
|
||||
]),
|
||||
),
|
||||
@@ -1082,6 +1103,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
.indexOf(kUsePermanentPassword)] &&
|
||||
(await bind.mainGetPermanentPassword())
|
||||
.isEmpty) {
|
||||
if (isChangePermanentPasswordDisabled()) {
|
||||
await callback();
|
||||
return;
|
||||
}
|
||||
setPasswordDialog(notEmptyCallback: callback);
|
||||
} else {
|
||||
await callback();
|
||||
@@ -1186,7 +1211,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
enabled: tmpEnabled && !locked),
|
||||
if (usePassword) numericOneTimePassword,
|
||||
if (usePassword) radios[1],
|
||||
if (usePassword)
|
||||
if (usePassword && !isChangePermanentPasswordDisabled())
|
||||
_SubButton('Set permanent password', setPasswordDialog,
|
||||
permEnabled && !locked),
|
||||
// if (usePassword)
|
||||
@@ -1205,11 +1230,14 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
...directIp(context),
|
||||
whitelist(),
|
||||
...autoDisconnect(context),
|
||||
_OptionCheckBox(context, 'keep-awake-during-incoming-sessions-label',
|
||||
kOptionKeepAwakeDuringIncomingSessions,
|
||||
reverse: false, enabled: enabled),
|
||||
if (bind.mainIsInstalled())
|
||||
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
|
||||
'allow-only-conn-window-open',
|
||||
reverse: false, enabled: enabled),
|
||||
if (bind.mainIsInstalled()) unlockPin()
|
||||
if (bind.mainIsInstalled() && !isUnlockPinDisabled()) unlockPin()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2645,7 +2673,7 @@ Widget _lock(
|
||||
]).marginSymmetric(vertical: 2)),
|
||||
onPressed: () async {
|
||||
final unlockPin = bind.mainGetUnlockPin();
|
||||
if (unlockPin.isEmpty) {
|
||||
if (unlockPin.isEmpty || isUnlockPinDisabled()) {
|
||||
bool checked = await callMainCheckSuperUserPermission();
|
||||
if (checked) {
|
||||
onUnlock();
|
||||
|
||||
@@ -17,7 +17,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart';
|
||||
|
||||
@@ -86,6 +85,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
|
||||
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
||||
final _overlayKeyState = OverlayKeyState();
|
||||
final _uniqueKey = UniqueKey();
|
||||
|
||||
late FFI _ffi;
|
||||
|
||||
@@ -107,9 +107,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Get.put<FFI>(_ffi, tag: 'ft_${widget.id}');
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
if (isWeb) {
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
@@ -127,9 +125,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
model.close().whenComplete(() {
|
||||
_ffi.close();
|
||||
_ffi.dialogManager.dismissAll();
|
||||
if (!isLinux) {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
||||
});
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
@@ -282,11 +278,9 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
item.state != JobState.inProgress,
|
||||
child: LinearPercentIndicator(
|
||||
animateFromLastPercent: true,
|
||||
center: Text(
|
||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||
),
|
||||
center: Text(item.percentText),
|
||||
barRadius: Radius.circular(15),
|
||||
percent: item.finishedSize / item.totalSize,
|
||||
percent: item.percent,
|
||||
progressColor: MyTheme.accent,
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
lineHeight: kDesktopFileTransferRowHeight,
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
@@ -16,6 +15,7 @@ import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/toolbar.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/input_model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
import '../../utils/image.dart';
|
||||
@@ -85,11 +85,16 @@ class _RemotePageState extends State<RemotePage>
|
||||
late RxBool _zoomCursor;
|
||||
late RxBool _remoteCursorMoved;
|
||||
late RxBool _keyboardEnabled;
|
||||
final _uniqueKey = UniqueKey();
|
||||
|
||||
var _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||
|
||||
// Debounce timer for pointer lock center updates during window events.
|
||||
// Uses kDefaultPointerLockCenterThrottleMs from consts.dart for the duration.
|
||||
Timer? _pointerLockCenterDebounceTimer;
|
||||
|
||||
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
||||
// to identify the toolbar instance and its callback function.
|
||||
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
||||
@@ -138,9 +143,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
@@ -171,6 +174,16 @@ class _RemotePageState extends State<RemotePage>
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.tabController?.onSelected?.call(widget.id);
|
||||
});
|
||||
|
||||
// Register callback to cancel debounce timer when relative mouse mode is disabled
|
||||
_ffi.inputModel.onRelativeMouseModeDisabled =
|
||||
_cancelPointerLockCenterDebounceTimer;
|
||||
}
|
||||
|
||||
/// Cancel the pointer lock center debounce timer
|
||||
void _cancelPointerLockCenterDebounceTimer() {
|
||||
_pointerLockCenterDebounceTimer?.cancel();
|
||||
_pointerLockCenterDebounceTimer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -186,6 +199,13 @@ class _RemotePageState extends State<RemotePage>
|
||||
_rawKeyFocusNode.unfocus();
|
||||
}
|
||||
stateGlobal.isFocused.value = false;
|
||||
|
||||
// When window loses focus, temporarily release relative mouse mode constraints
|
||||
// to allow user to interact with other applications normally.
|
||||
// The cursor will be re-hidden and re-centered when window regains focus.
|
||||
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||
_ffi.inputModel.onWindowBlur();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -196,6 +216,12 @@ class _RemotePageState extends State<RemotePage>
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
stateGlobal.isFocused.value = true;
|
||||
|
||||
// Restore relative mouse mode constraints when window regains focus.
|
||||
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||
_rawKeyFocusNode.requestFocus();
|
||||
_ffi.inputModel.onWindowFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -206,25 +232,59 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
// Update pointer lock center when window is restored
|
||||
_updatePointerLockCenterIfNeeded();
|
||||
}
|
||||
|
||||
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
super.onWindowMaximize();
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
// Update pointer lock center when window is maximized
|
||||
_updatePointerLockCenterIfNeeded();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
super.onWindowResize();
|
||||
// Update pointer lock center when window is resized
|
||||
_updatePointerLockCenterIfNeeded();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMove() {
|
||||
super.onWindowMove();
|
||||
// Update pointer lock center when window is moved
|
||||
_updatePointerLockCenterIfNeeded();
|
||||
}
|
||||
|
||||
/// Update pointer lock center with debouncing to avoid excessive updates
|
||||
/// during rapid window move/resize events.
|
||||
void _updatePointerLockCenterIfNeeded() {
|
||||
if (!_ffi.inputModel.relativeMouseMode.value) return;
|
||||
|
||||
// Cancel any pending update and schedule a new one (debounce pattern)
|
||||
_pointerLockCenterDebounceTimer?.cancel();
|
||||
_pointerLockCenterDebounceTimer = Timer(
|
||||
const Duration(milliseconds: kDefaultPointerLockCenterThrottleMs),
|
||||
() {
|
||||
if (!mounted) return;
|
||||
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||
_ffi.inputModel.updatePointerLockCenter();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
super.onWindowMinimize();
|
||||
if (!isLinux) {
|
||||
WakelockPlus.disable();
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
// Release cursor constraints when minimized
|
||||
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||
_ffi.inputModel.onWindowBlur();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +311,16 @@ class _RemotePageState extends State<RemotePage>
|
||||
// https://github.com/flutter/flutter/issues/64935
|
||||
super.dispose();
|
||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||
|
||||
// Defensive cleanup: ensure host system-key propagation is reset even if
|
||||
// MouseRegion.onExit never fired (e.g., tab closed while cursor inside).
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
|
||||
_pointerLockCenterDebounceTimer?.cancel();
|
||||
_pointerLockCenterDebounceTimer = null;
|
||||
// Clear callback reference to prevent memory leaks and stale references
|
||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||
if (closeSession) {
|
||||
// ensure we leave this session, this is a double check
|
||||
@@ -268,9 +338,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
if (!isLinux) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
await Get.delete<FFI>(tag: widget.id);
|
||||
removeSharedStates(widget.id);
|
||||
}
|
||||
@@ -354,10 +422,15 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
}(),
|
||||
// Use Overlay to enable rebuild every time on menu button click.
|
||||
_ffi.ffiModel.pi.isSet.isTrue
|
||||
? Overlay(
|
||||
initialEntries: [OverlayEntry(builder: remoteToolbar)])
|
||||
: remoteToolbar(context),
|
||||
// Hide toolbar when relative mouse mode is active to prevent
|
||||
// cursor from escaping to toolbar area.
|
||||
Obx(() => _ffi.inputModel.relativeMouseMode.value
|
||||
? const Offstage()
|
||||
: _ffi.ffiModel.pi.isSet.isTrue
|
||||
? Overlay(initialEntries: [
|
||||
OverlayEntry(builder: remoteToolbar)
|
||||
])
|
||||
: remoteToolbar(context)),
|
||||
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
||||
],
|
||||
),
|
||||
@@ -425,6 +498,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
// See [onWindowBlur].
|
||||
if (!isWindows) {
|
||||
if (!_rawKeyFocusNode.hasFocus) {
|
||||
@@ -450,6 +524,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
// See [onWindowBlur].
|
||||
if (!isWindows) {
|
||||
_ffi.inputModel.enterOrLeave(false);
|
||||
@@ -497,33 +572,39 @@ class _RemotePageState extends State<RemotePage>
|
||||
|
||||
Widget getBodyForDesktop(BuildContext context) {
|
||||
var paints = <Widget>[
|
||||
MouseRegion(onEnter: (evt) {
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
||||
}, onExit: (evt) {
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||
final c = Provider.of<CanvasModel>(context, listen: false);
|
||||
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||
return Obx(
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
? Container(color: Colors.transparent)
|
||||
: Obx(() {
|
||||
widget.toolbarState.initShow(sessionId);
|
||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
|
||||
child, enterView, leaveView),
|
||||
ffi: _ffi,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}))
|
||||
MouseRegion(
|
||||
onEnter: (evt) {
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
||||
},
|
||||
onExit: (evt) {
|
||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
},
|
||||
child: _ViewStyleUpdater(
|
||||
canvasModel: _ffi.canvasModel,
|
||||
inputModel: _ffi.inputModel,
|
||||
child: Builder(builder: (context) {
|
||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||
return Obx(
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
? Container(color: Colors.transparent)
|
||||
: Obx(() {
|
||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
zoomCursor: _zoomCursor,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
remoteCursorMoved: _remoteCursorMoved,
|
||||
listenerBuilder: (child) =>
|
||||
_buildRawTouchAndPointerRegion(
|
||||
child, enterView, leaveView),
|
||||
ffi: _ffi,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||
@@ -552,6 +633,63 @@ class _RemotePageState extends State<RemotePage>
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
/// A widget that tracks the view size and updates CanvasModel.updateViewStyle()
|
||||
/// and InputModel.updateImageWidgetSize() only when size actually changes.
|
||||
/// This avoids scheduling post-frame callbacks on every LayoutBuilder rebuild.
|
||||
class _ViewStyleUpdater extends StatefulWidget {
|
||||
final CanvasModel canvasModel;
|
||||
final InputModel inputModel;
|
||||
final Widget child;
|
||||
|
||||
const _ViewStyleUpdater({
|
||||
Key? key,
|
||||
required this.canvasModel,
|
||||
required this.inputModel,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_ViewStyleUpdater> createState() => _ViewStyleUpdaterState();
|
||||
}
|
||||
|
||||
class _ViewStyleUpdaterState extends State<_ViewStyleUpdater> {
|
||||
Size? _lastSize;
|
||||
bool _callbackScheduled = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxHeight = constraints.maxHeight;
|
||||
// Guard against infinite constraints (e.g., unconstrained ancestor).
|
||||
if (!maxWidth.isFinite || !maxHeight.isFinite) {
|
||||
return widget.child;
|
||||
}
|
||||
final newSize = Size(maxWidth, maxHeight);
|
||||
if (_lastSize != newSize) {
|
||||
_lastSize = newSize;
|
||||
// Schedule the update for after the current frame to avoid setState during build.
|
||||
// Use _callbackScheduled flag to prevent accumulating multiple callbacks
|
||||
// when size changes rapidly before any callback executes.
|
||||
if (!_callbackScheduled) {
|
||||
_callbackScheduled = true;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_callbackScheduled = false;
|
||||
final currentSize = _lastSize;
|
||||
if (mounted && currentSize != null) {
|
||||
widget.canvasModel.updateViewStyle();
|
||||
widget.inputModel.updateImageWidgetSize(currentSize);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return widget.child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImagePaint extends StatefulWidget {
|
||||
final FFI ffi;
|
||||
final String id;
|
||||
@@ -616,21 +754,24 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
cursor: cursorOverImage.isTrue
|
||||
? c.cursorEmbedded
|
||||
? SystemMouseCursors.none
|
||||
: keyboardEnabled.isTrue
|
||||
? (() {
|
||||
if (remoteCursorMoved.isTrue) {
|
||||
_lastRemoteCursorMoved = true;
|
||||
return SystemMouseCursors.none;
|
||||
} else {
|
||||
if (_lastRemoteCursorMoved) {
|
||||
_lastRemoteCursorMoved = false;
|
||||
_firstEnterImage.value = true;
|
||||
}
|
||||
return _buildCustomCursor(
|
||||
context, getCursorScale());
|
||||
}
|
||||
}())
|
||||
: _buildDisabledCursor(context, getCursorScale())
|
||||
// Hide cursor when relative mouse mode is active
|
||||
: widget.ffi.inputModel.relativeMouseMode.value
|
||||
? SystemMouseCursors.none
|
||||
: keyboardEnabled.isTrue
|
||||
? (() {
|
||||
if (remoteCursorMoved.isTrue) {
|
||||
_lastRemoteCursorMoved = true;
|
||||
return SystemMouseCursors.none;
|
||||
} else {
|
||||
if (_lastRemoteCursorMoved) {
|
||||
_lastRemoteCursorMoved = false;
|
||||
_firstEnterImage.value = true;
|
||||
}
|
||||
return _buildCustomCursor(
|
||||
context, getCursorScale());
|
||||
}
|
||||
}())
|
||||
: _buildDisabledCursor(context, getCursorScale())
|
||||
: MouseCursor.defer,
|
||||
onHover: (evt) {},
|
||||
child: child);
|
||||
|
||||
@@ -135,7 +135,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
onWindowCloseButton: handleWindowCloseButton,
|
||||
tail: const AddButton(),
|
||||
tail: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_RelativeMouseModeHint(tabController: tabController),
|
||||
const AddButton(),
|
||||
],
|
||||
),
|
||||
selectedBorderColor: MyTheme.accent,
|
||||
pageViewBuilder: (pageView) => pageView,
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
@@ -251,11 +257,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||
translate(
|
||||
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'),
|
||||
style: style,
|
||||
)),
|
||||
proc: () {
|
||||
toolbarState.switchShow(sessionId);
|
||||
toolbarState.switchHide(sessionId);
|
||||
cancelFunc();
|
||||
},
|
||||
padding: padding,
|
||||
@@ -374,6 +380,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
loopCloseWindow();
|
||||
}
|
||||
ConnectionTypeState.delete(id);
|
||||
// Clean up relative mouse mode state for this peer.
|
||||
stateGlobal.relativeMouseModeState.remove(id);
|
||||
_update_remote_count();
|
||||
}
|
||||
|
||||
@@ -548,3 +556,69 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that displays a hint in the tab bar when relative mouse mode is active.
|
||||
/// This helps users remember how to exit relative mouse mode.
|
||||
class _RelativeMouseModeHint extends StatelessWidget {
|
||||
final DesktopTabController tabController;
|
||||
|
||||
const _RelativeMouseModeHint({Key? key, required this.tabController})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
// Check if there are any tabs
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Get current selected tab's RemotePage
|
||||
final selectedTabInfo = tabController.state.value.selectedTabInfo;
|
||||
if (selectedTabInfo.page is! RemotePage) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final remotePage = selectedTabInfo.page as RemotePage;
|
||||
final String peerId = remotePage.id;
|
||||
|
||||
// Use global state to check relative mouse mode (synced from InputModel).
|
||||
// This avoids timing issues with FFI registration.
|
||||
final isRelativeMouseMode =
|
||||
stateGlobal.relativeMouseModeState[peerId] ?? false;
|
||||
|
||||
if (!isRelativeMouseMode) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.mouse,
|
||||
size: 14,
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
translate(
|
||||
'rel-mouse-exit-{${isMacOS ? "Cmd+G" : "Ctrl+Alt"}}-tip'),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/remote_input.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
import '../../consts.dart';
|
||||
@@ -77,6 +76,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
String keyboardMode = "legacy";
|
||||
bool _isWindowBlur = false;
|
||||
final _cursorOverImage = false.obs;
|
||||
final _uniqueKey = UniqueKey();
|
||||
|
||||
var _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
@@ -124,9 +124,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
|
||||
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
||||
@@ -185,26 +183,20 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
if (isWindows) {
|
||||
_isWindowBlur = false;
|
||||
}
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
}
|
||||
|
||||
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
super.onWindowMaximize();
|
||||
if (!isLinux) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
super.onWindowMinimize();
|
||||
if (!isLinux) {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -247,9 +239,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
if (!isLinux) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
await Get.delete<FFI>(tag: widget.id);
|
||||
removeSharedStates(widget.id);
|
||||
}
|
||||
@@ -465,7 +455,6 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||
? Container(color: Colors.transparent)
|
||||
: Obx(() {
|
||||
widget.toolbarState.initShow(sessionId);
|
||||
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
|
||||
@@ -250,11 +250,11 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||
translate(
|
||||
toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'),
|
||||
style: style,
|
||||
)),
|
||||
proc: () {
|
||||
toolbarState.switchShow(sessionId);
|
||||
toolbarState.switchHide(sessionId);
|
||||
cancelFunc();
|
||||
},
|
||||
padding: padding,
|
||||
|
||||
@@ -31,8 +31,12 @@ import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
|
||||
class ToolbarState {
|
||||
late RxBool _pin;
|
||||
|
||||
bool isShowInited = false;
|
||||
RxBool show = false.obs;
|
||||
RxBool collapse = false.obs;
|
||||
RxBool hide = false.obs;
|
||||
|
||||
// Track initialization state to prevent flickering
|
||||
final RxBool initialized = false.obs;
|
||||
bool _isInitializing = false;
|
||||
|
||||
ToolbarState() {
|
||||
_pin = RxBool(false);
|
||||
@@ -53,19 +57,39 @@ class ToolbarState {
|
||||
|
||||
bool get pin => _pin.value;
|
||||
|
||||
switchShow(SessionID sessionId) async {
|
||||
bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionCollapseToolbar);
|
||||
show.value = !show.value;
|
||||
/// Initialize all toolbar states from session options.
|
||||
/// This should be called once when the toolbar is first created.
|
||||
Future<void> init(SessionID sessionId) async {
|
||||
if (initialized.value || _isInitializing) return;
|
||||
_isInitializing = true;
|
||||
|
||||
try {
|
||||
// Load both states in parallel for better performance
|
||||
final results = await Future.wait([
|
||||
bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionCollapseToolbar),
|
||||
bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionHideToolbar),
|
||||
]);
|
||||
|
||||
collapse.value = results[0] ?? false;
|
||||
hide.value = results[1] ?? false;
|
||||
} finally {
|
||||
_isInitializing = false;
|
||||
initialized.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
initShow(SessionID sessionId) async {
|
||||
if (!isShowInited) {
|
||||
show.value = !(await bind.sessionGetToggleOption(
|
||||
sessionId: sessionId, arg: kOptionCollapseToolbar) ??
|
||||
false);
|
||||
isShowInited = true;
|
||||
}
|
||||
switchCollapse(SessionID sessionId) async {
|
||||
bind.sessionToggleOption(
|
||||
sessionId: sessionId, value: kOptionCollapseToolbar);
|
||||
collapse.value = !collapse.value;
|
||||
}
|
||||
|
||||
// Switch hide state for entire toolbar visibility
|
||||
switchHide(SessionID sessionId) async {
|
||||
bind.sessionToggleOption(sessionId: sessionId, value: kOptionHideToolbar);
|
||||
hide.value = !hide.value;
|
||||
}
|
||||
|
||||
switchPin() async {
|
||||
@@ -237,7 +261,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
// setState(() {});
|
||||
}
|
||||
|
||||
RxBool get show => widget.state.show;
|
||||
RxBool get collapse => widget.state.collapse;
|
||||
RxBool get hide => widget.state.hide;
|
||||
bool get pin => widget.state.pin;
|
||||
|
||||
PeerInfo get pi => widget.ffi.ffiModel.pi;
|
||||
@@ -258,6 +283,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
arg: 'remote-menubar-drag-x') ??
|
||||
'0.5') ??
|
||||
0.5;
|
||||
// Initialize toolbar states (collapse, hide) from session options
|
||||
widget.state.init(widget.ffi.sessionId);
|
||||
});
|
||||
|
||||
_debouncerHide = Debouncer<int>(
|
||||
@@ -277,8 +304,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
}
|
||||
|
||||
_debouncerHideProc(int v) {
|
||||
if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) {
|
||||
show.value = false;
|
||||
if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) {
|
||||
collapse.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,17 +318,27 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Obx(() => show.value
|
||||
? _buildToolbar(context)
|
||||
: _buildDraggableShowHide(context)),
|
||||
);
|
||||
return Obx(() {
|
||||
// Wait for initialization to complete to prevent flickering
|
||||
if (!widget.state.initialized.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// If toolbar is hidden, return empty widget
|
||||
if (hide.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: collapse.isFalse
|
||||
? _buildToolbar(context)
|
||||
: _buildDraggableCollapse(context),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDraggableShowHide(BuildContext context) {
|
||||
Widget _buildDraggableCollapse(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (show.isTrue && _dragging.isFalse) {
|
||||
if (collapse.isFalse && _dragging.isFalse) {
|
||||
triggerAutoHide();
|
||||
}
|
||||
final borderRadius = BorderRadius.vertical(
|
||||
@@ -398,7 +435,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDraggableShowHide(context),
|
||||
_buildDraggableCollapse(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1728,13 +1765,23 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var ffiModel = Provider.of<FfiModel>(context);
|
||||
if (!ffiModel.keyboard) return Offstage();
|
||||
toolbarToggles() => toolbarKeyboardToggles(ffi)
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi))
|
||||
.toList();
|
||||
toolbarToggles() {
|
||||
final toggles = toolbarKeyboardToggles(ffi)
|
||||
.map((e) => CkbMenuButton(
|
||||
value: e.value,
|
||||
onChanged: e.onChanged,
|
||||
child: e.child,
|
||||
ffi: ffi) as Widget)
|
||||
.toList();
|
||||
if (toggles.isNotEmpty) {
|
||||
toggles.add(Divider());
|
||||
}
|
||||
return toggles;
|
||||
}
|
||||
|
||||
return _IconSubmenuButton(
|
||||
tooltip: 'Keyboard Settings',
|
||||
svg: "assets/keyboard.svg",
|
||||
svg: "assets/keyboard_mouse.svg",
|
||||
ffi: ffi,
|
||||
color: _ToolbarTheme.blueColor,
|
||||
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||
@@ -2491,7 +2538,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
double left = 0.0;
|
||||
double right = 1.0;
|
||||
|
||||
RxBool get show => widget.toolbarState.show;
|
||||
RxBool get collapse => widget.toolbarState.collapse;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
@@ -2614,20 +2661,20 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
||||
)),
|
||||
buttonWrapper(
|
||||
() => setState(() {
|
||||
widget.toolbarState.switchShow(widget.sessionId);
|
||||
widget.toolbarState.switchCollapse(widget.sessionId);
|
||||
}),
|
||||
Obx((() => Tooltip(
|
||||
message:
|
||||
translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
message: translate(
|
||||
collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'),
|
||||
child: Icon(
|
||||
show.isTrue ? Icons.expand_less : Icons.expand_more,
|
||||
collapse.isFalse ? Icons.expand_less : Icons.expand_more,
|
||||
size: iconSize,
|
||||
),
|
||||
))),
|
||||
),
|
||||
if (isWebDesktop)
|
||||
Obx(() {
|
||||
if (show.isTrue) {
|
||||
if (collapse.isFalse) {
|
||||
return Offstage();
|
||||
} else {
|
||||
return buttonWrapper(
|
||||
|
||||
@@ -593,7 +593,6 @@ class _DesktopTabState extends State<DesktopTab>
|
||||
|
||||
Widget _buildBar() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
@@ -72,6 +71,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
showLocal ? model.localController : model.remoteController;
|
||||
FileDirectory get currentDir => currentFileController.directory.value;
|
||||
DirectoryOptions get currentOptions => currentFileController.options.value;
|
||||
final _uniqueKey = UniqueKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -86,7 +86,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, widget.id);
|
||||
WakelockPlus.enable();
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -94,7 +94,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
model.close().whenComplete(() {
|
||||
gFFI.close();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
WakelockPlus.disable();
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
});
|
||||
model.jobController.clear();
|
||||
super.dispose();
|
||||
@@ -355,15 +355,21 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
switch (jobTable.last.state) {
|
||||
// Find the first job that is in progress (the one actually transferring data)
|
||||
// Rust backend processes jobs sequentially, so the first inProgress job is the active one
|
||||
final activeJob = jobTable
|
||||
.firstWhereOrNull((job) => job.state == JobState.inProgress) ??
|
||||
jobTable.last;
|
||||
|
||||
switch (activeJob.state) {
|
||||
case JobState.inProgress:
|
||||
return BottomSheetBody(
|
||||
leading: CircularProgressIndicator(),
|
||||
title: translate("Waiting"),
|
||||
text:
|
||||
"${translate("Speed")}: ${readableFileSize(jobTable.last.speed)}/s",
|
||||
"${translate("Speed")}: ${readableFileSize(activeJob.speed)}/s",
|
||||
onCanceled: () {
|
||||
model.jobController.cancelJob(jobTable.last.id);
|
||||
model.jobController.cancelJob(activeJob.id);
|
||||
jobTable.clear();
|
||||
},
|
||||
);
|
||||
@@ -371,7 +377,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
return BottomSheetBody(
|
||||
leading: Icon(Icons.check),
|
||||
title: "${translate("Successful")}!",
|
||||
text: jobTable.last.display(),
|
||||
text: activeJob.display(),
|
||||
onCanceled: () => jobTable.clear(),
|
||||
);
|
||||
case JobState.error:
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@@ -67,7 +66,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
String _value = '';
|
||||
Orientation? _currentOrientation;
|
||||
double _viewInsetsBottom = 0;
|
||||
|
||||
final _uniqueKey = UniqueKey();
|
||||
Timer? _timerDidChangeMetrics;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
@@ -105,9 +104,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!isWeb) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
_physicalFocusNode.requestFocus();
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
@@ -146,9 +143,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
if (!isWeb) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
await keyboardSubscription.cancel();
|
||||
removeSharedStates(widget.id);
|
||||
// `on_voice_call_closed` should be called when the connection is ended.
|
||||
@@ -569,7 +564,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
bool get showCursorPaint =>
|
||||
!gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded;
|
||||
!gFFI.ffiModel.isPeerAndroid &&
|
||||
!gFFI.canvasModel.cursorEmbedded &&
|
||||
!gFFI.inputModel.relativeMouseMode.value;
|
||||
|
||||
Widget getBodyForMobile() {
|
||||
final keyboardIsVisible = keyboardVisibilityController.isVisible;
|
||||
@@ -808,6 +805,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
bind.mainSetLocalOption(key: kOptionTouchMode, value: v);
|
||||
},
|
||||
virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
|
||||
inputModel: gFFI.inputModel,
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -61,12 +61,13 @@ class _DropDownAction extends StatelessWidget {
|
||||
final isAllowNumericOneTimePassword =
|
||||
gFFI.serverModel.allowNumericOneTimePassword;
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: gFFI.serverModel.connectStatus > 0,
|
||||
value: "changeID",
|
||||
child: Text(translate("Change ID")),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
if (!isChangeIdDisabled())
|
||||
PopupMenuItem(
|
||||
enabled: gFFI.serverModel.connectStatus > 0,
|
||||
value: "changeID",
|
||||
child: Text(translate("Change ID")),
|
||||
),
|
||||
if (!isChangeIdDisabled()) const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
value: 'AcceptSessionsViaPassword',
|
||||
child: listTile(
|
||||
@@ -87,7 +88,8 @@ class _DropDownAction extends StatelessWidget {
|
||||
),
|
||||
if (showPasswordOption) const PopupMenuDivider(),
|
||||
if (showPasswordOption &&
|
||||
verificationMethod != kUseTemporaryPassword)
|
||||
verificationMethod != kUseTemporaryPassword &&
|
||||
!isChangePermanentPasswordDisabled())
|
||||
PopupMenuItem(
|
||||
value: "setPermanentPassword",
|
||||
child: Text(translate("Set permanent password")),
|
||||
@@ -149,6 +151,10 @@ class _DropDownAction extends StatelessWidget {
|
||||
|
||||
if (value == kUsePermanentPassword &&
|
||||
(await bind.mainGetPermanentPassword()).isEmpty) {
|
||||
if (isChangePermanentPasswordDisabled()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
setPasswordDialog(notEmptyCallback: callback);
|
||||
} else {
|
||||
callback();
|
||||
@@ -648,9 +654,8 @@ class ConnectionManager extends StatelessWidget {
|
||||
return Column(
|
||||
children: serverModel.clients
|
||||
.map((client) => PaddingCard(
|
||||
title: translate(client.isFileTransfer
|
||||
? "Transfer file"
|
||||
: "Share screen"),
|
||||
title: translate(
|
||||
client.isFileTransfer ? "Transfer file" : "Share screen"),
|
||||
titleIcon: client.isFileTransfer
|
||||
? Icon(Icons.folder_outlined)
|
||||
: Icon(Icons.mobile_screen_share),
|
||||
|
||||
@@ -71,6 +71,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _ignoreBatteryOpt = false;
|
||||
var _enableStartOnBoot = false;
|
||||
var _checkUpdateOnStartup = false;
|
||||
var _showTerminalExtraKeys = false;
|
||||
var _floatingWindowDisabled = false;
|
||||
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
||||
var _enableAbr = false;
|
||||
@@ -99,6 +100,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
var _enableIpv6Punch = false;
|
||||
var _isUsingPublicServer = false;
|
||||
var _allowAskForNoteAtEndOfConnection = false;
|
||||
var _preventSleepWhileConnected = true;
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
@@ -139,6 +141,10 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
||||
_allowAskForNoteAtEndOfConnection =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
||||
_preventSleepWhileConnected =
|
||||
mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions);
|
||||
_showTerminalExtraKeys =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -602,6 +608,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
|
||||
enhancementsTiles.add(
|
||||
SettingsTile.switchTile(
|
||||
initialValue: _showTerminalExtraKeys,
|
||||
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(translate('Show terminal extra keys')),
|
||||
]),
|
||||
onToggle: (bool v) async {
|
||||
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
|
||||
final newValue =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
setState(() {
|
||||
_showTerminalExtraKeys = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
onFloatingWindowChanged(bool toValue) async {
|
||||
if (toValue) {
|
||||
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||
@@ -786,19 +809,35 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
showThemeSettings(gFFI.dialogManager);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('note-at-conn-end-tip')),
|
||||
initialValue: _allowAskForNoteAtEndOfConnection,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, v);
|
||||
final newValue = mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection);
|
||||
setState(() {
|
||||
_allowAskForNoteAtEndOfConnection = newValue;
|
||||
});
|
||||
},
|
||||
)
|
||||
if (!bind.isDisableAccount())
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('note-at-conn-end-tip')),
|
||||
initialValue: _allowAskForNoteAtEndOfConnection,
|
||||
onToggle: (v) async {
|
||||
if (v && !gFFI.userModel.isLogin) {
|
||||
final res = await loginDialog();
|
||||
if (res != true) return;
|
||||
}
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionAllowAskForNoteAtEndOfConnection, v);
|
||||
final newValue = mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection);
|
||||
setState(() {
|
||||
_allowAskForNoteAtEndOfConnection = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('keep-awake-during-outgoing-sessions-label')),
|
||||
initialValue: _preventSleepWhileConnected,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v);
|
||||
setState(() {
|
||||
_preventSleepWhileConnected = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
]),
|
||||
if (isAndroid)
|
||||
SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -7,6 +10,7 @@ import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import '../../desktop/pages/terminal_connection_manager.dart';
|
||||
import '../../consts.dart';
|
||||
|
||||
class TerminalPage extends StatefulWidget {
|
||||
const TerminalPage({
|
||||
@@ -29,9 +33,18 @@ class TerminalPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TerminalPageState extends State<TerminalPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
double _sysKeyboardHeight = 0;
|
||||
Timer? _keyboardDebounce;
|
||||
final GlobalKey _keyboardKey = GlobalKey();
|
||||
double _keyboardHeight = 0;
|
||||
late bool _showTerminalExtraKeys;
|
||||
// For iOS edge swipe gesture
|
||||
double _swipeStartX = 0;
|
||||
double _swipeCurrentX = 0;
|
||||
|
||||
// For web only.
|
||||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||||
@@ -44,6 +57,7 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
debugPrint(
|
||||
'[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}');
|
||||
@@ -62,13 +76,22 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
debugPrint(
|
||||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||||
|
||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||
_cellHeight = ph * 1.0;
|
||||
};
|
||||
|
||||
// Register this terminal model with FFI for event routing
|
||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||
|
||||
_showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
// Initialize terminal connection
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
|
||||
if (_showTerminalExtraKeys) {
|
||||
_updateKeyboardHeight();
|
||||
}
|
||||
});
|
||||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||||
}
|
||||
@@ -78,10 +101,43 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
_keyboardDebounce?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
super.didChangeMetrics();
|
||||
|
||||
_keyboardDebounce?.cancel();
|
||||
_keyboardDebounce = Timer(const Duration(milliseconds: 20), () {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
setState(() {
|
||||
_sysKeyboardHeight = bottomInset;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _updateKeyboardHeight() {
|
||||
if (_keyboardKey.currentContext != null) {
|
||||
final renderBox = _keyboardKey.currentContext!.findRenderObject() as RenderBox;
|
||||
_keyboardHeight = renderBox.size.height;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsets _calculatePadding(double heightPx) {
|
||||
if (_cellHeight == null) {
|
||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||
}
|
||||
final realHeight = heightPx - _sysKeyboardHeight - _keyboardHeight;
|
||||
final rows = (realHeight / _cellHeight!).floor();
|
||||
final extraSpace = realHeight - rows * _cellHeight!;
|
||||
final topBottom = max(0.0, extraSpace / 2.0);
|
||||
return EdgeInsets.only(left: 5.0, right: 5.0, top: topBottom, bottom: topBottom + _sysKeyboardHeight + _keyboardHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@@ -95,31 +151,275 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
return Scaffold(
|
||||
final scaffold = Scaffold(
|
||||
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
textStyle: _getTerminalStyle(),
|
||||
backgroundOpacity: 0.7,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: SafeArea(
|
||||
top: true,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final heightPx = constraints.maxHeight;
|
||||
return TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
textStyle: _getTerminalStyle(),
|
||||
backgroundOpacity: 0.7,
|
||||
// The following comment is from xterm.dart source code:
|
||||
// Workaround to detect delete key for platforms and IMEs that do not
|
||||
// emit a hardware delete event. Preferred on mobile platforms. [false] by
|
||||
// default.
|
||||
//
|
||||
// Android works fine without this workaround.
|
||||
deleteDetection: isIOS,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
if (selection != null) {
|
||||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||||
_terminalModel.terminalController.clearSelection();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
} else {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text != null) {
|
||||
_terminalModel.terminal.paste(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
|
||||
// iOS-style circular close button in top-right corner
|
||||
if (isIOS) _buildCloseButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Add iOS edge swipe gesture to exit (similar to Android back button)
|
||||
if (isIOS) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final screenWidth = constraints.maxWidth;
|
||||
// Base thresholds on screen width but clamp to reasonable logical pixel ranges
|
||||
// Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels
|
||||
final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0);
|
||||
// Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels
|
||||
final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0);
|
||||
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
|
||||
() => HorizontalDragGestureRecognizer(
|
||||
debugOwner: this,
|
||||
// Only respond to touch input, exclude mouse/trackpad
|
||||
supportedDevices: kTouchBasedDeviceKinds,
|
||||
),
|
||||
(HorizontalDragGestureRecognizer instance) {
|
||||
instance
|
||||
// Capture initial touch-down position (before touch slop)
|
||||
..onDown = (details) {
|
||||
_swipeStartX = details.localPosition.dx;
|
||||
_swipeCurrentX = details.localPosition.dx;
|
||||
}
|
||||
..onUpdate = (details) {
|
||||
_swipeCurrentX = details.localPosition.dx;
|
||||
}
|
||||
..onEnd = (details) {
|
||||
// Check if swipe started from left edge and moved right
|
||||
if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) {
|
||||
clientClose(sessionId, _ffi);
|
||||
}
|
||||
_swipeStartX = 0;
|
||||
_swipeCurrentX = 0;
|
||||
}
|
||||
..onCancel = () {
|
||||
_swipeStartX = 0;
|
||||
_swipeCurrentX = 0;
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
child: scaffold,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
minimum: const EdgeInsets.only(
|
||||
top: 16, // iOS standard margin
|
||||
right: 16, // iOS standard margin
|
||||
),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: translate('Close'),
|
||||
child: Container(
|
||||
width: 44, // iOS standard tap target size
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5), // Half transparency
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
shape: const CircleBorder(),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
clientClose(sessionId, _ffi);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: translate('Close'),
|
||||
child: const Icon(
|
||||
Icons.chevron_left, // iOS-style back arrow
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFloatingKeyboard() {
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: _sysKeyboardHeight,
|
||||
child: Container(
|
||||
key: _keyboardKey,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildKeyButton('Esc'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('/'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('|'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('Home'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('↑'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('End'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('PgUp'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildKeyButton('Tab'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('Ctrl+C'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('~'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('←'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('↓'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('→'),
|
||||
const SizedBox(width: 2),
|
||||
_buildKeyButton('PgDn'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeyButton(String label) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
_sendKeyToTerminal(label);
|
||||
},
|
||||
child: Text(label),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(48, 32),
|
||||
padding: EdgeInsets.zero,
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendKeyToTerminal(String key) {
|
||||
String? send;
|
||||
|
||||
switch (key) {
|
||||
case 'Esc':
|
||||
send = '\x1B';
|
||||
break;
|
||||
case 'Tab':
|
||||
send = '\t';
|
||||
break;
|
||||
case 'Ctrl+C':
|
||||
send = '\x03';
|
||||
break;
|
||||
|
||||
case '↑':
|
||||
send = '\x1B[A';
|
||||
break;
|
||||
case '↓':
|
||||
send = '\x1B[B';
|
||||
break;
|
||||
case '→':
|
||||
send = '\x1B[C';
|
||||
break;
|
||||
case '←':
|
||||
send = '\x1B[D';
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
send = '\x1B[H';
|
||||
break;
|
||||
case 'End':
|
||||
send = '\x1B[F';
|
||||
break;
|
||||
case 'PgUp':
|
||||
send = '\x1B[5~';
|
||||
break;
|
||||
case 'PgDn':
|
||||
send = '\x1B[6~';
|
||||
break;
|
||||
|
||||
default:
|
||||
send = key;
|
||||
break;
|
||||
}
|
||||
|
||||
if (send != null) {
|
||||
_terminalModel.sendVirtualKey(send);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/overlay.dart';
|
||||
@@ -62,7 +61,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
bool _showGestureHelp = false;
|
||||
Orientation? _currentOrientation;
|
||||
double _viewInsetsBottom = 0;
|
||||
|
||||
final _uniqueKey = UniqueKey();
|
||||
Timer? _timerDidChangeMetrics;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
@@ -100,9 +99,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
gFFI.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
if (!isWeb) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
_physicalFocusNode.requestFocus();
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
@@ -139,9 +136,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
if (!isWeb) {
|
||||
await WakelockPlus.disable();
|
||||
}
|
||||
WakelockManager.disable(_uniqueKey);
|
||||
removeSharedStates(widget.id);
|
||||
// `on_voice_call_closed` should be called when the connection is ended.
|
||||
// The inner logic of `on_voice_call_closed` will check if the voice call is active.
|
||||
|
||||
@@ -83,7 +83,10 @@ class _FloatingMouseWidgetsState extends State<FloatingMouseWidgets> {
|
||||
cursorModel: _cursorModel,
|
||||
),
|
||||
if (virtualMouseMode.showVirtualJoystick)
|
||||
VirtualJoystick(cursorModel: _cursorModel),
|
||||
VirtualJoystick(
|
||||
cursorModel: _cursorModel,
|
||||
inputModel: _inputModel,
|
||||
),
|
||||
FloatingLeftRightButton(
|
||||
isLeft: true,
|
||||
inputModel: _inputModel,
|
||||
@@ -674,12 +677,18 @@ class _QuarterCirclePainter extends CustomPainter {
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
// Virtual joystick sends the absolute movement for now.
|
||||
// Maybe we need to change it to relative movement in the future.
|
||||
// Virtual joystick can send either absolute movement (via updatePan)
|
||||
// or relative movement (via sendMobileRelativeMouseMove) depending on the
|
||||
// InputModel.relativeMouseMode setting.
|
||||
class VirtualJoystick extends StatefulWidget {
|
||||
final CursorModel cursorModel;
|
||||
final InputModel inputModel;
|
||||
|
||||
const VirtualJoystick({super.key, required this.cursorModel});
|
||||
const VirtualJoystick({
|
||||
super.key,
|
||||
required this.cursorModel,
|
||||
required this.inputModel,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VirtualJoystick> createState() => _VirtualJoystickState();
|
||||
@@ -694,6 +703,10 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
||||
final double _moveStep = 3.0;
|
||||
final double _speed = 1.0;
|
||||
|
||||
/// Scale factor for relative mouse movement sensitivity.
|
||||
/// Higher values result in faster cursor movement on the remote machine.
|
||||
static const double _kRelativeMouseScale = 3.0;
|
||||
|
||||
// One-shot timer to detect a drag gesture
|
||||
Timer? _dragStartTimer;
|
||||
// Periodic timer for continuous movement
|
||||
@@ -701,6 +714,9 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
||||
Size? _lastScreenSize;
|
||||
bool _isPressed = false;
|
||||
|
||||
/// Check if relative mouse mode is enabled.
|
||||
bool get _useRelativeMouse => widget.inputModel.relativeMouseMode.value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -746,6 +762,18 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Send movement delta to remote machine.
|
||||
/// Uses relative mouse mode if enabled, otherwise uses absolute updatePan.
|
||||
void _sendMovement(Offset delta) {
|
||||
if (_useRelativeMouse) {
|
||||
widget.inputModel.sendMobileRelativeMouseMove(
|
||||
delta.dx * _kRelativeMouseScale, delta.dy * _kRelativeMouseScale);
|
||||
} else {
|
||||
// In absolute mode, use cursorModel.updatePan which tracks position.
|
||||
widget.cursorModel.updatePan(delta, Offset.zero, false);
|
||||
}
|
||||
}
|
||||
|
||||
void _stopSendEventTimer() {
|
||||
_dragStartTimer?.cancel();
|
||||
_continuousMoveTimer?.cancel();
|
||||
@@ -773,7 +801,7 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
||||
// The movement is small for a gentle start.
|
||||
final initialDelta = _offsetToPanDelta(_offset);
|
||||
if (initialDelta.distance > 0) {
|
||||
widget.cursorModel.updatePan(initialDelta, Offset.zero, false);
|
||||
_sendMovement(initialDelta);
|
||||
}
|
||||
|
||||
// 2. Start a one-shot timer to check if the user is holding for a drag.
|
||||
@@ -784,10 +812,7 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
||||
_continuousMoveTimer =
|
||||
periodic_immediate(const Duration(milliseconds: 20), () async {
|
||||
if (_offset != Offset.zero) {
|
||||
widget.cursorModel.updatePan(
|
||||
_offsetToPanDelta(_offset) * _moveStep * _speed,
|
||||
Offset.zero,
|
||||
false);
|
||||
_sendMovement(_offsetToPanDelta(_offset) * _moveStep * _speed);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
class GestureIcons {
|
||||
@@ -39,11 +41,13 @@ class GestureHelp extends StatefulWidget {
|
||||
{Key? key,
|
||||
required this.touchMode,
|
||||
required this.onTouchModeChange,
|
||||
required this.virtualMouseMode})
|
||||
required this.virtualMouseMode,
|
||||
this.inputModel})
|
||||
: super(key: key);
|
||||
final bool touchMode;
|
||||
final OnTouchModeChange onTouchModeChange;
|
||||
final VirtualMouseMode virtualMouseMode;
|
||||
final InputModel? inputModel;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() =>
|
||||
@@ -61,6 +65,14 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
_selectedIndex = _touchMode ? 1 : 0;
|
||||
}
|
||||
|
||||
/// Helper to exit relative mouse mode when certain conditions are met.
|
||||
/// This reduces code duplication across multiple UI callbacks.
|
||||
void _exitRelativeMouseModeIf(bool condition) {
|
||||
if (condition) {
|
||||
widget.inputModel?.setRelativeMouseMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
@@ -103,6 +115,8 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
_selectedIndex = index ?? 0;
|
||||
_touchMode = index == 0 ? false : true;
|
||||
widget.onTouchModeChange(_touchMode);
|
||||
// Exit relative mouse mode when switching to touch mode
|
||||
_exitRelativeMouseModeIf(_touchMode);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -117,12 +131,18 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
await _virtualMouseMode.toggleVirtualMouse();
|
||||
// Exit relative mouse mode when virtual mouse is hidden
|
||||
_exitRelativeMouseModeIf(
|
||||
!_virtualMouseMode.showVirtualMouse);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
await _virtualMouseMode.toggleVirtualMouse();
|
||||
// Exit relative mouse mode when virtual mouse is hidden
|
||||
_exitRelativeMouseModeIf(
|
||||
!_virtualMouseMode.showVirtualMouse);
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(translate('Show virtual mouse')),
|
||||
@@ -196,6 +216,10 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
if (value == null) return;
|
||||
await _virtualMouseMode
|
||||
.toggleVirtualJoystick();
|
||||
// Exit relative mouse mode when joystick is hidden
|
||||
_exitRelativeMouseModeIf(
|
||||
!_virtualMouseMode
|
||||
.showVirtualJoystick);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
@@ -203,6 +227,10 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
onTap: () async {
|
||||
await _virtualMouseMode
|
||||
.toggleVirtualJoystick();
|
||||
// Exit relative mouse mode when joystick is hidden
|
||||
_exitRelativeMouseModeIf(
|
||||
!_virtualMouseMode
|
||||
.showVirtualJoystick);
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
@@ -211,6 +239,39 @@ class _GestureHelpState extends State<GestureHelp> {
|
||||
],
|
||||
)),
|
||||
),
|
||||
// Relative mouse mode option - only visible when joystick is shown
|
||||
if (!_touchMode &&
|
||||
_virtualMouseMode.showVirtualMouse &&
|
||||
_virtualMouseMode.showVirtualJoystick &&
|
||||
widget.inputModel != null)
|
||||
Obx(() => Transform.translate(
|
||||
offset: const Offset(-10.0, -24.0),
|
||||
child: Padding(
|
||||
// Indent further for 'Relative mouse mode'
|
||||
padding: const EdgeInsets.only(left: 48.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: widget.inputModel!
|
||||
.relativeMouseMode.value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
widget.inputModel!
|
||||
.setRelativeMouseMode(value);
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.inputModel!
|
||||
.toggleRelativeMouseMode();
|
||||
},
|
||||
child: Text(
|
||||
translate('Relative mouse mode')),
|
||||
),
|
||||
],
|
||||
)),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -202,6 +202,7 @@ class AbModel {
|
||||
final api = "${await bind.mainGetApiServer()}/api/ab/settings";
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
_setEmptyBody(headers);
|
||||
final resp = await http.post(Uri.parse(api), headers: headers);
|
||||
if (resp.statusCode == 404) {
|
||||
debugPrint("HTTP 404, api server doesn't support shared address book");
|
||||
@@ -228,6 +229,7 @@ class AbModel {
|
||||
final api = "${await bind.mainGetApiServer()}/api/ab/personal";
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
_setEmptyBody(headers);
|
||||
final resp = await http.post(Uri.parse(api), headers: headers);
|
||||
if (resp.statusCode == 404) {
|
||||
debugPrint("HTTP 404, current api server is legacy mode");
|
||||
@@ -269,6 +271,7 @@ class AbModel {
|
||||
});
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
_setEmptyBody(headers);
|
||||
final resp = await http.post(uri, headers: headers);
|
||||
Map<String, dynamic> json =
|
||||
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
|
||||
@@ -1012,16 +1015,8 @@ class LegacyAb extends BaseAb {
|
||||
var authHeaders = getHttpHeaders();
|
||||
authHeaders['Content-Type'] = "application/json";
|
||||
final body = jsonEncode({"data": jsonEncode(_serialize())});
|
||||
http.Response resp;
|
||||
// support compression
|
||||
if (licensedDevices > 0 && body.length > 1024) {
|
||||
authHeaders['Content-Encoding'] = "gzip";
|
||||
resp = await http.post(Uri.parse(api),
|
||||
headers: authHeaders, body: GZipCodec().encode(utf8.encode(body)));
|
||||
} else {
|
||||
resp =
|
||||
await http.post(Uri.parse(api), headers: authHeaders, body: body);
|
||||
}
|
||||
http.Response resp =
|
||||
await http.post(Uri.parse(api), headers: authHeaders, body: body);
|
||||
if (resp.statusCode == 200 &&
|
||||
(resp.body.isEmpty || resp.body.toLowerCase() == 'null')) {
|
||||
ret = true;
|
||||
@@ -1406,6 +1401,7 @@ class Ab extends BaseAb {
|
||||
});
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
_setEmptyBody(headers);
|
||||
final resp = await http.post(uri, headers: headers);
|
||||
statusCode = resp.statusCode;
|
||||
Map<String, dynamic> json =
|
||||
@@ -1463,6 +1459,7 @@ class Ab extends BaseAb {
|
||||
);
|
||||
var headers = getHttpHeaders();
|
||||
headers['Content-Type'] = "application/json";
|
||||
_setEmptyBody(headers);
|
||||
final resp = await http.post(uri, headers: headers);
|
||||
statusCode = resp.statusCode;
|
||||
List<dynamic> json =
|
||||
@@ -1977,3 +1974,8 @@ String _jsonDecodeActionResp(http.Response resp) {
|
||||
}
|
||||
return errMsg;
|
||||
}
|
||||
|
||||
// https://github.com/seanmonstar/reqwest/issues/838
|
||||
void _setEmptyBody(Map<String, String> headers) {
|
||||
headers['Content-Length'] = '0';
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ class TransferJobSerdeData {
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
id: int.tryParse(d['id'].toString()) ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
path: d['dataSource'] ?? '',
|
||||
isRemote: d['isRemote'] ?? false,
|
||||
totalSize: d['totalSize'] ?? 0,
|
||||
finishedSize: d['finishedSize'] ?? 0,
|
||||
|
||||
@@ -113,6 +113,34 @@ class FileModel {
|
||||
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
|
||||
}
|
||||
|
||||
// This method fixes a deadlock that occurred when the previous code directly
|
||||
// called jobController.jobError(evt) in the job_error event handler.
|
||||
//
|
||||
// The problem with directly calling jobController.jobError():
|
||||
// 1. fetchDirectoryRecursiveToRemove(jobID) registers readRecursiveTasks[jobID]
|
||||
// and waits for completion
|
||||
// 2. If the remote has no permission (or some other errors), it returns a FileTransferError
|
||||
// 3. The error triggers job_error event, which called jobController.jobError()
|
||||
// 4. jobController.jobError() calls getJob(jobID) to find the job in jobTable
|
||||
// 5. But addDeleteDirJob() is called AFTER fetchDirectoryRecursiveToRemove(),
|
||||
// so the job doesn't exist yet in jobTable
|
||||
// 6. Result: jobController.jobError() does nothing useful, and
|
||||
// readRecursiveTasks[jobID] never completes, causing a 2s timeout
|
||||
//
|
||||
// Solution: Before calling jobController.jobError(), we first check if there's
|
||||
// a pending readRecursiveTasks with this ID and complete it with the error.
|
||||
void handleJobError(Map<String, dynamic> evt) {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id != null) {
|
||||
final err = evt['err']?.toString() ?? 'Unknown error';
|
||||
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
||||
}
|
||||
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
||||
// even if the event does not have a valid job ID. This allows for generic error handling
|
||||
// or logging of unexpected errors.
|
||||
jobController.jobError(evt);
|
||||
}
|
||||
|
||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||
evtLoop.pushEvent(
|
||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||
@@ -591,8 +619,21 @@ class FileController {
|
||||
} else if (item.isDirectory) {
|
||||
title = translate("Not an empty directory");
|
||||
dialogManager?.showLoading(translate("Waiting"));
|
||||
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
final FileDirectory fd;
|
||||
try {
|
||||
fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
} catch (e) {
|
||||
dialogManager?.dismissAll();
|
||||
final dm = dialogManager;
|
||||
if (dm != null) {
|
||||
msgBox(sessionId, 'custom-error-nook-nocancel-hasclose',
|
||||
translate("Error"), e.toString(), '', dm);
|
||||
} else {
|
||||
debugPrint("removeAction error msgbox failed: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fd.path.isEmpty) {
|
||||
fd.path = item.path;
|
||||
}
|
||||
@@ -606,7 +647,7 @@ class FileController {
|
||||
item.name,
|
||||
false);
|
||||
if (confirm == true) {
|
||||
sendRemoveEmptyDir(
|
||||
await sendRemoveEmptyDir(
|
||||
item.path,
|
||||
0,
|
||||
deleteJobId,
|
||||
@@ -647,7 +688,7 @@ class FileController {
|
||||
// handle remove res;
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
await sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
@@ -660,7 +701,7 @@ class FileController {
|
||||
final res = await jobController.jobResultListener.start();
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
await sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -755,9 +796,9 @@ class FileController {
|
||||
fileNum: fileNum);
|
||||
}
|
||||
|
||||
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||
Future<void> sendRemoveEmptyDir(String path, int fileNum, int actId) async {
|
||||
history.removeWhere((element) => element.contains(path));
|
||||
bind.sessionRemoveAllEmptyDirs(
|
||||
await bind.sessionRemoveAllEmptyDirs(
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
@@ -1275,6 +1316,15 @@ class FileFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
// Complete a pending recursive read task with an error.
|
||||
// See FileModel.handleJobError() for why this is necessary.
|
||||
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
||||
final completer = readRecursiveTasks.remove(id);
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
completer.completeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FileDirectory>> readEmptyDirs(
|
||||
String path, bool isLocal, bool showHidden) async {
|
||||
try {
|
||||
@@ -1438,6 +1488,10 @@ class JobProgress {
|
||||
var err = "";
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
double get percent =>
|
||||
totalSize > 0 ? (finishedSize.toDouble() / totalSize) : 0.0;
|
||||
String get percentText => '${(percent * 100).toStringAsFixed(0)}%';
|
||||
|
||||
clear() {
|
||||
type = JobType.none;
|
||||
state = JobState.none;
|
||||
|
||||
@@ -14,6 +14,8 @@ import 'package:get/get.dart';
|
||||
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/state_model.dart';
|
||||
import 'relative_mouse_model.dart';
|
||||
import '../common.dart';
|
||||
import '../consts.dart';
|
||||
|
||||
@@ -57,7 +59,8 @@ class CanvasCoords {
|
||||
model.scale = json['scale'];
|
||||
model.scrollX = json['scrollX'];
|
||||
model.scrollY = json['scrollY'];
|
||||
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.scrollStyle =
|
||||
ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
||||
model.size = Size(json['size']['w'], json['size']['h']);
|
||||
return model;
|
||||
}
|
||||
@@ -349,15 +352,28 @@ class InputModel {
|
||||
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
||||
var _trackpadScrollUnsent = Offset.zero;
|
||||
|
||||
// Mobile relative mouse delta accumulators (for slow/fine movements).
|
||||
double _mobileDeltaRemainderX = 0.0;
|
||||
double _mobileDeltaRemainderY = 0.0;
|
||||
|
||||
var _lastScale = 1.0;
|
||||
|
||||
bool _pointerMovedAfterEnter = false;
|
||||
bool _pointerInsideImage = false;
|
||||
|
||||
// mouse
|
||||
final isPhysicalMouse = false.obs;
|
||||
int _lastButtons = 0;
|
||||
Offset lastMousePos = Offset.zero;
|
||||
|
||||
// Relative mouse mode (for games/3D apps).
|
||||
final relativeMouseMode = false.obs;
|
||||
late final RelativeMouseModel _relativeMouse;
|
||||
// Callback to cancel external throttle timer when relative mouse mode is disabled.
|
||||
VoidCallback? onRelativeMouseModeDisabled;
|
||||
// Disposer for the relativeMouseMode observer (to prevent memory leaks).
|
||||
Worker? _relativeMouseModeDisposer;
|
||||
|
||||
bool _queryOtherWindowCoords = false;
|
||||
Rect? _windowRect;
|
||||
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
||||
@@ -367,15 +383,108 @@ class InputModel {
|
||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||
String get id => parent.target?.id ?? '';
|
||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||
String get peerVersion => parent.target?.ffiModel.pi.version ?? '';
|
||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
|
||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||
int get trackpadSpeed => _trackpadSpeed;
|
||||
bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
||||
bool get useEdgeScroll =>
|
||||
parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
||||
|
||||
/// Check if the connected server supports relative mouse mode.
|
||||
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
||||
|
||||
InputModel(this.parent) {
|
||||
sessionId = parent.target!.sessionId;
|
||||
_relativeMouse = RelativeMouseModel(
|
||||
sessionId: sessionId,
|
||||
enabled: relativeMouseMode,
|
||||
keyboardPerm: () => keyboardPerm,
|
||||
isViewCamera: () => isViewCamera,
|
||||
peerVersion: () => peerVersion,
|
||||
peerPlatform: () => peerPlatform,
|
||||
modify: (msg) => modify(msg),
|
||||
getPointerInsideImage: () => _pointerInsideImage,
|
||||
setPointerInsideImage: (inside) => _pointerInsideImage = inside,
|
||||
);
|
||||
_relativeMouse.onDisabled = () => onRelativeMouseModeDisabled?.call();
|
||||
|
||||
// Sync relative mouse mode state to global state for UI components (e.g., tab bar hint).
|
||||
_relativeMouseModeDisposer = ever(relativeMouseMode, (bool value) {
|
||||
final peerId = id;
|
||||
if (peerId.isNotEmpty) {
|
||||
stateGlobal.relativeMouseModeState[peerId] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157241
|
||||
// Infer CapsLock state from the character output.
|
||||
// This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report
|
||||
// incorrect CapsLock state on iOS.
|
||||
bool _getIosCapsFromCharacter(KeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(
|
||||
ch, HardwareKeyboard.instance.isShiftPressed);
|
||||
}
|
||||
|
||||
// RawKeyEvent version of _getIosCapsFromCharacter.
|
||||
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
|
||||
}
|
||||
|
||||
// Shared implementation for inferring CapsLock state from character.
|
||||
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
|
||||
//
|
||||
// Limitations:
|
||||
// 1. This inference assumes the client and server use the same keyboard layout.
|
||||
// If layouts differ (e.g., client uses EN, server uses DE), the character output
|
||||
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
|
||||
// layout, making it impossible to correctly infer CapsLock state from the
|
||||
// character alone.
|
||||
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
|
||||
// produces lowercase). This method cannot handle that case correctly.
|
||||
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
|
||||
if (ch == null || ch.length != 1) return false;
|
||||
// Use Dart's built-in Unicode-aware case detection
|
||||
final upper = ch.toUpperCase();
|
||||
final lower = ch.toLowerCase();
|
||||
final isUpper = upper == ch && lower != ch;
|
||||
final isLower = lower == ch && upper != ch;
|
||||
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
|
||||
if (!isUpper && !isLower) return false;
|
||||
return isUpper != shiftPressed;
|
||||
}
|
||||
|
||||
int _buildLockModes(bool iosCapsLock) {
|
||||
const capslock = 1;
|
||||
const numlock = 2;
|
||||
const scrolllock = 3;
|
||||
int lockModes = 0;
|
||||
if (isIOS) {
|
||||
if (iosCapsLock) {
|
||||
lockModes |= (1 << capslock);
|
||||
}
|
||||
// Ignore "NumLock/ScrollLock" on iOS for now.
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return lockModes;
|
||||
}
|
||||
|
||||
// This function must be called after the peer info is received.
|
||||
@@ -506,6 +615,15 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
if (_relativeMouse.handleRawKeyEvent(e)) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && e is RawKeyDownEvent) {
|
||||
iosCapsLock = _getIosCapsFromRawCharacter(e);
|
||||
}
|
||||
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (!e.repeat) {
|
||||
@@ -542,7 +660,7 @@ class InputModel {
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
||||
mapKeyboardModeRaw(e);
|
||||
mapKeyboardModeRaw(e, iosCapsLock);
|
||||
} else {
|
||||
legacyKeyboardModeRaw(e);
|
||||
}
|
||||
@@ -568,6 +686,21 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
if (_relativeMouse.handleKeyEvent(
|
||||
e,
|
||||
ctrlPressed: ctrl,
|
||||
shiftPressed: shift,
|
||||
altPressed: alt,
|
||||
commandPressed: command,
|
||||
)) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
|
||||
iosCapsLock = _getIosCapsFromCharacter(e);
|
||||
}
|
||||
|
||||
if (e is KeyUpEvent) {
|
||||
handleKeyUpEventModifiers(e);
|
||||
} else if (e is KeyDownEvent) {
|
||||
@@ -613,7 +746,8 @@ class InputModel {
|
||||
e.character ?? '',
|
||||
e.physicalKey.usbHidUsage & 0xFFFF,
|
||||
// Show repeat event be converted to "release+press" events?
|
||||
e is KeyDownEvent || e is KeyRepeatEvent);
|
||||
e is KeyDownEvent || e is KeyRepeatEvent,
|
||||
iosCapsLock);
|
||||
} else {
|
||||
legacyKeyboardMode(e);
|
||||
}
|
||||
@@ -622,23 +756,9 @@ class InputModel {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
void newKeyboardMode(
|
||||
String character, int usbHid, bool down, bool iosCapsLock) {
|
||||
final lockModes = _buildLockModes(iosCapsLock);
|
||||
bind.sessionHandleFlutterKeyEvent(
|
||||
sessionId: sessionId,
|
||||
character: character,
|
||||
@@ -647,7 +767,7 @@ class InputModel {
|
||||
downOrUp: down);
|
||||
}
|
||||
|
||||
void mapKeyboardModeRaw(RawKeyEvent e) {
|
||||
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
|
||||
int positionCode = -1;
|
||||
int platformCode = -1;
|
||||
bool down;
|
||||
@@ -678,27 +798,14 @@ class InputModel {
|
||||
} else {
|
||||
down = false;
|
||||
}
|
||||
inputRawKey(e.character ?? '', platformCode, positionCode, down);
|
||||
inputRawKey(
|
||||
e.character ?? '', platformCode, positionCode, down, iosCapsLock);
|
||||
}
|
||||
|
||||
/// Send raw Key Event
|
||||
void inputRawKey(String name, int platformCode, int positionCode, 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);
|
||||
}
|
||||
void inputRawKey(String name, int platformCode, int positionCode, bool down,
|
||||
bool iosCapsLock) {
|
||||
final lockModes = _buildLockModes(iosCapsLock);
|
||||
bind.sessionHandleFlutterRawKeyEvent(
|
||||
sessionId: sessionId,
|
||||
name: name,
|
||||
@@ -772,6 +879,9 @@ class InputModel {
|
||||
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
|
||||
final Map<String, dynamic> out = {};
|
||||
|
||||
bool hasStaleButtonsOnMouseUp =
|
||||
type == _kMouseEventUp && evt.buttons == _lastButtons;
|
||||
|
||||
// Check update event type and set buttons to be sent.
|
||||
int buttons = _lastButtons;
|
||||
if (type == _kMouseEventMove) {
|
||||
@@ -796,7 +906,7 @@ class InputModel {
|
||||
buttons = evt.buttons;
|
||||
}
|
||||
}
|
||||
_lastButtons = evt.buttons;
|
||||
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
|
||||
|
||||
out['buttons'] = buttons;
|
||||
out['type'] = type;
|
||||
@@ -853,11 +963,13 @@ class InputModel {
|
||||
toReleaseKeys.release(handleKeyEvent);
|
||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||
_pointerMovedAfterEnter = false;
|
||||
_pointerInsideImage = enter;
|
||||
|
||||
// Fix status
|
||||
if (!enter) {
|
||||
resetModifiers();
|
||||
}
|
||||
_relativeMouse.onEnterOrLeaveImage(enter);
|
||||
_flingTimer?.cancel();
|
||||
if (!isInputSourceFlutter) {
|
||||
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
||||
@@ -878,15 +990,142 @@ class InputModel {
|
||||
msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
|
||||
}
|
||||
|
||||
/// Send relative mouse movement for mobile clients (virtual joystick).
|
||||
/// This method is for touch-based controls that want to send delta values.
|
||||
/// Uses the 'move_relative' type which bypasses absolute position tracking.
|
||||
///
|
||||
/// Accumulates fractional deltas to avoid losing slow/fine movements.
|
||||
/// Only sends events when relative mouse mode is enabled and supported.
|
||||
Future<void> sendMobileRelativeMouseMove(double dx, double dy) async {
|
||||
if (!keyboardPerm) return;
|
||||
if (isViewCamera) return;
|
||||
// Only send relative mouse events when relative mode is enabled and supported.
|
||||
if (!isRelativeMouseModeSupported || !relativeMouseMode.value) return;
|
||||
_mobileDeltaRemainderX += dx;
|
||||
_mobileDeltaRemainderY += dy;
|
||||
final x = _mobileDeltaRemainderX.truncate();
|
||||
final y = _mobileDeltaRemainderY.truncate();
|
||||
_mobileDeltaRemainderX -= x;
|
||||
_mobileDeltaRemainderY -= y;
|
||||
if (x == 0 && y == 0) return;
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(modify({
|
||||
'type': 'move_relative',
|
||||
'x': '$x',
|
||||
'y': '$y',
|
||||
})));
|
||||
}
|
||||
|
||||
/// Update the pointer lock center position based on current window frame.
|
||||
Future<void> updatePointerLockCenter({Offset? localCenter}) {
|
||||
return _relativeMouse.updatePointerLockCenter(localCenter: localCenter);
|
||||
}
|
||||
|
||||
/// Get the current image widget size (for comparison to avoid unnecessary updates).
|
||||
Size? get imageWidgetSize => _relativeMouse.imageWidgetSize;
|
||||
|
||||
/// Update the image widget size for center calculation.
|
||||
void updateImageWidgetSize(Size size) {
|
||||
_relativeMouse.updateImageWidgetSize(size);
|
||||
}
|
||||
|
||||
void toggleRelativeMouseMode() {
|
||||
_relativeMouse.toggleRelativeMouseMode();
|
||||
}
|
||||
|
||||
bool setRelativeMouseMode(bool enabled) {
|
||||
return _relativeMouse.setRelativeMouseMode(enabled);
|
||||
}
|
||||
|
||||
/// Exit relative mouse mode and release all modifier keys to the remote.
|
||||
/// This is called when the user presses the exit shortcut (Ctrl+Alt on Win/Linux, Cmd+G on macOS).
|
||||
/// We need to send key-up events for all modifiers because the shortcut itself may have
|
||||
/// blocked some key events, leaving the remote in a state where modifiers are stuck.
|
||||
void exitRelativeMouseModeWithKeyRelease() {
|
||||
if (!_relativeMouse.enabled.value) return;
|
||||
|
||||
// First, send release events for all modifier keys to the remote.
|
||||
// This ensures the remote doesn't have stuck modifier keys after exiting.
|
||||
// Use press: false, down: false to send key-up events without modifiers attached.
|
||||
final modifiersToRelease = [
|
||||
'Control_L',
|
||||
'Control_R',
|
||||
'Alt_L',
|
||||
'Alt_R',
|
||||
'Shift_L',
|
||||
'Shift_R',
|
||||
'Meta_L', // Command/Super left
|
||||
'Meta_R', // Command/Super right
|
||||
];
|
||||
|
||||
for (final key in modifiersToRelease) {
|
||||
bind.sessionInputKey(
|
||||
sessionId: sessionId,
|
||||
name: key,
|
||||
down: false,
|
||||
press: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
command: false,
|
||||
);
|
||||
}
|
||||
|
||||
// Reset local modifier state
|
||||
resetModifiers();
|
||||
|
||||
// Now exit relative mouse mode
|
||||
_relativeMouse.setRelativeMouseMode(false);
|
||||
}
|
||||
|
||||
void disposeRelativeMouseMode() {
|
||||
_relativeMouse.dispose();
|
||||
onRelativeMouseModeDisabled = null;
|
||||
// Cancel the relative mouse mode observer and clean up global state.
|
||||
_relativeMouseModeDisposer?.dispose();
|
||||
_relativeMouseModeDisposer = null;
|
||||
final peerId = id;
|
||||
if (peerId.isNotEmpty) {
|
||||
stateGlobal.relativeMouseModeState.remove(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
void onWindowBlur() {
|
||||
_relativeMouse.onWindowBlur();
|
||||
}
|
||||
|
||||
void onWindowFocus() {
|
||||
_relativeMouse.onWindowFocus();
|
||||
}
|
||||
|
||||
void onPointHoverImage(PointerHoverEvent e) {
|
||||
_stopFling = true;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
|
||||
// May fix https://github.com/rustdesk/rustdesk/issues/13009
|
||||
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
|
||||
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
|
||||
// Ignore this event to prevent cursor jumping.
|
||||
debugPrint('Ignored synthesized hover at (0,0) on iOS');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update pointer region when relative mouse mode is enabled.
|
||||
// This avoids unnecessary tracking when not in relative mode.
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||
}
|
||||
|
||||
if (!isPhysicalMouse.value) {
|
||||
isPhysicalMouse.value = true;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
||||
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
||||
edgeScroll: useEdgeScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,6 +1274,28 @@ class InputModel {
|
||||
_trackpadLastDelta = Offset.zero;
|
||||
}
|
||||
|
||||
// iOS Magic Mouse duplicate event detection.
|
||||
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
|
||||
// for the same click in certain areas (like top-left corner).
|
||||
int _lastMouseDownTimeMs = 0;
|
||||
ui.Offset _lastMouseDownPos = ui.Offset.zero;
|
||||
|
||||
/// Check if a touch tap event should be ignored because it's a duplicate
|
||||
/// of a recent mouse event (iOS Magic Mouse issue).
|
||||
bool shouldIgnoreTouchTap(ui.Offset pos) {
|
||||
if (!isIOS) return false;
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
final dt = nowMs - _lastMouseDownTimeMs;
|
||||
final distance = (_lastMouseDownPos - pos).distance;
|
||||
// If touch tap is within 2000ms and 80px of the last mouse down,
|
||||
// it's likely a duplicate event from the same Magic Mouse click.
|
||||
if (dt >= 0 && dt < 2000 && distance < 80.0) {
|
||||
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onPointDownImage(PointerDownEvent e) {
|
||||
debugPrint("onPointDownImage ${e.kind}");
|
||||
_stopFling = true;
|
||||
@@ -1043,13 +1304,32 @@ class InputModel {
|
||||
_windowRect = null;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
|
||||
// Track mouse down events for duplicate detection on iOS.
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
if (e.kind == ui.PointerDeviceKind.mouse) {
|
||||
_lastMouseDownTimeMs = nowMs;
|
||||
_lastMouseDownPos = e.position;
|
||||
}
|
||||
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||
}
|
||||
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||
if (isPhysicalMouse.value) {
|
||||
isPhysicalMouse.value = false;
|
||||
}
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
|
||||
// In relative mouse mode, send button events without position.
|
||||
// Use _relativeMouse.enabled.value consistently with the guard above.
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse
|
||||
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown));
|
||||
} else {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,9 +1337,21 @@ class InputModel {
|
||||
if (isDesktop) _queryOtherWindowCoords = false;
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||
}
|
||||
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
||||
// In relative mouse mode, send button events without position.
|
||||
// Use _relativeMouse.enabled.value consistently with the guard above.
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse
|
||||
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp));
|
||||
} else {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,6 +1359,11 @@ class InputModel {
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (isViewCamera) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
|
||||
if (_relativeMouse.enabled.value) {
|
||||
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||
}
|
||||
|
||||
if (_queryOtherWindowCoords) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
||||
@@ -1074,7 +1371,10 @@ class InputModel {
|
||||
_queryOtherWindowCoords = false;
|
||||
}
|
||||
if (isPhysicalMouse.value) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
||||
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
||||
edgeScroll: useEdgeScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1098,6 +1398,11 @@ class InputModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Handle scroll/wheel events.
|
||||
/// Note: Scroll events intentionally use absolute positioning even in relative mouse mode.
|
||||
/// This is because scroll events don't need relative positioning - they represent
|
||||
/// scroll deltas that are independent of cursor position. Games and 3D applications
|
||||
/// handle scroll events the same way regardless of mouse mode.
|
||||
void onPointerSignalImage(PointerSignalEvent e) {
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
@@ -1285,14 +1590,18 @@ class InputModel {
|
||||
evt['y'] = '${pos.y.toInt()}';
|
||||
}
|
||||
|
||||
Map<int, String> mapButtons = {
|
||||
kPrimaryMouseButton: 'left',
|
||||
kSecondaryMouseButton: 'right',
|
||||
kMiddleMouseButton: 'wheel',
|
||||
kBackMouseButton: 'back',
|
||||
kForwardMouseButton: 'forward'
|
||||
};
|
||||
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
|
||||
final buttons = evt['buttons'];
|
||||
if (buttons is int) {
|
||||
evt['buttons'] = mouseButtonsToPeer(buttons);
|
||||
} else {
|
||||
// Log warning if buttons exists but is not an int (unexpected caller).
|
||||
// Keep empty string fallback for missing buttons to preserve move/hover behavior.
|
||||
if (buttons != null) {
|
||||
debugPrint(
|
||||
'[InputModel] processEventToPeer: unexpected buttons type: ${buttons.runtimeType}, value: $buttons');
|
||||
}
|
||||
evt['buttons'] = '';
|
||||
}
|
||||
return evt;
|
||||
}
|
||||
|
||||
@@ -1303,8 +1612,8 @@ class InputModel {
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final evtToPeer =
|
||||
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
||||
final evtToPeer = processEventToPeer(evt, offset,
|
||||
onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
||||
if (evtToPeer != null) {
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
|
||||
@@ -1544,9 +1853,9 @@ class InputModel {
|
||||
// Simulate a key press event.
|
||||
// `usbHidUsage` is the USB HID usage code of the key.
|
||||
Future<void> tapHidKey(int usbHidUsage) async {
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
|
||||
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false);
|
||||
}
|
||||
|
||||
Future<void> onMobileVolumeUp() async =>
|
||||
|
||||
@@ -120,6 +120,7 @@ class FfiModel with ChangeNotifier {
|
||||
late VirtualMouseMode virtualMouseMode;
|
||||
Timer? _timer;
|
||||
var _reconnects = 1;
|
||||
DateTime? _offlineReconnectStartTime;
|
||||
bool _viewOnly = false;
|
||||
bool _showMyCursor = false;
|
||||
WeakReference<FFI> parent;
|
||||
@@ -213,6 +214,9 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
updatePermission(Map<String, dynamic> evt, String id) {
|
||||
// Track previous keyboard permission to detect revocation.
|
||||
final hadKeyboardPerm = _permissions['keyboard'] != false;
|
||||
|
||||
evt.forEach((k, v) {
|
||||
if (k == 'name' || k.isEmpty) return;
|
||||
_permissions[k] = v == 'true';
|
||||
@@ -221,6 +225,18 @@ class FfiModel with ChangeNotifier {
|
||||
if (parent.target?.connType == ConnType.defaultConn) {
|
||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||
}
|
||||
|
||||
// If keyboard permission was revoked while relative mouse mode is active,
|
||||
// forcefully disable relative mouse mode to prevent the user from being trapped.
|
||||
final hasKeyboardPerm = _permissions['keyboard'] != false;
|
||||
if (hadKeyboardPerm && !hasKeyboardPerm) {
|
||||
final inputModel = parent.target?.inputModel;
|
||||
if (inputModel != null && inputModel.relativeMouseMode.value) {
|
||||
inputModel.setRelativeMouseMode(false);
|
||||
showToast(translate('rel-mouse-permission-lost-tip'));
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('updatePermission: $_permissions');
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -363,7 +379,7 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.refreshAll();
|
||||
}
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
parent.target?.fileModel.handleJobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
parent.target?.fileModel.postOverrideFileConfirm(evt);
|
||||
} else if (name == 'load_last_job') {
|
||||
@@ -457,6 +473,9 @@ class FfiModel with ChangeNotifier {
|
||||
_handlePrinterRequest(evt, sessionId, peerId);
|
||||
} else if (name == 'screenshot') {
|
||||
_handleScreenshot(evt, sessionId, peerId);
|
||||
} else if (name == 'exit_relative_mouse_mode') {
|
||||
// Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS)
|
||||
parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease();
|
||||
} else {
|
||||
debugPrint('Event is not handled in the fixed branch: $name');
|
||||
}
|
||||
@@ -765,7 +784,8 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
|
||||
Future<void> updateCurDisplay(SessionID sessionId,
|
||||
{updateCursorPos = false}) async {
|
||||
final newRect = displaysRect();
|
||||
if (newRect == null) {
|
||||
return;
|
||||
@@ -777,9 +797,19 @@ class FfiModel with ChangeNotifier {
|
||||
updateCursorPos: updateCursorPos);
|
||||
}
|
||||
_rect = newRect;
|
||||
parent.target?.canvasModel
|
||||
// Await updateViewStyle to ensure view geometry is fully updated before
|
||||
// updating pointer lock center. This prevents stale center calculations.
|
||||
await parent.target?.canvasModel
|
||||
.updateViewStyle(refreshMousePos: updateCursorPos);
|
||||
_updateSessionWidthHeight(sessionId);
|
||||
|
||||
// Keep pointer lock center in sync when using relative mouse mode.
|
||||
// Note: updatePointerLockCenter is async-safe (handles errors internally),
|
||||
// so we fire-and-forget here.
|
||||
final inputModel = parent.target?.inputModel;
|
||||
if (inputModel != null && inputModel.relativeMouseMode.value) {
|
||||
inputModel.updatePointerLockCenter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +893,17 @@ class FfiModel with ChangeNotifier {
|
||||
final title = evt['title'];
|
||||
final text = evt['text'];
|
||||
final link = evt['link'];
|
||||
|
||||
// Disable relative mouse mode on any error-type message to ensure cursor is released.
|
||||
// This includes connection errors, session-ending messages, elevation errors, etc.
|
||||
// Safety: releasing pointer lock on errors prevents the user from being stuck.
|
||||
if (title == 'Connection Error' ||
|
||||
type == 'error' ||
|
||||
type == 'restarting' ||
|
||||
(type is String && type.contains('error'))) {
|
||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||
}
|
||||
|
||||
if (type == 're-input-password') {
|
||||
wrongPasswordDialog(sessionId, dialogManager, type, title, text);
|
||||
} else if (type == 'input-2fa') {
|
||||
@@ -900,11 +941,46 @@ class FfiModel with ChangeNotifier {
|
||||
showPrivacyFailedDialog(
|
||||
sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
} else {
|
||||
final hasRetry = evt['hasRetry'] == 'true';
|
||||
var hasRetry = evt['hasRetry'] == 'true';
|
||||
if (!hasRetry) {
|
||||
hasRetry = shouldAutoRetryOnOffline(type, title, text);
|
||||
}
|
||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-retry check for "Remote desktop is offline" error.
|
||||
/// returns true to auto-retry, false otherwise.
|
||||
bool shouldAutoRetryOnOffline(
|
||||
String type,
|
||||
String title,
|
||||
String text,
|
||||
) {
|
||||
if (type == 'error' &&
|
||||
title == 'Connection Error' &&
|
||||
text == 'Remote desktop is offline' &&
|
||||
_pi.isSet.isTrue) {
|
||||
// Auto retry for ~30s (server's peer offline threshold) when controlled peer's account changes
|
||||
// (e.g., signout, switch user, login into OS) causes temporary offline via websocket/tcp connection.
|
||||
// The actual wait may exceed 30s (e.g., 20s elapsed + 16s next retry = 36s), which is acceptable
|
||||
// since the controlled side reconnects quickly after account changes.
|
||||
// Uses time-based check instead of _reconnects count because user can manually retry.
|
||||
// https://github.com/rustdesk/rustdesk/discussions/14048
|
||||
if (_offlineReconnectStartTime == null) {
|
||||
// First offline, record time and start retry
|
||||
_offlineReconnectStartTime = DateTime.now();
|
||||
return true;
|
||||
} else {
|
||||
final elapsed =
|
||||
DateTime.now().difference(_offlineReconnectStartTime!).inSeconds;
|
||||
if (elapsed < 30) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleToast(Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||
final type = evt['type'] ?? 'info';
|
||||
final text = evt['text'] ?? '';
|
||||
@@ -962,11 +1038,14 @@ class FfiModel with ChangeNotifier {
|
||||
_reconnects *= 2;
|
||||
} else {
|
||||
_reconnects = 1;
|
||||
_offlineReconnectStartTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
||||
bool forceRelay) {
|
||||
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
||||
clearPermissions();
|
||||
dialogManager.dismissAll();
|
||||
@@ -1081,7 +1160,8 @@ class FfiModel with ChangeNotifier {
|
||||
if (displays.length == 1) {
|
||||
bind.sessionSetSize(
|
||||
sessionId: sessionId,
|
||||
display: pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
display:
|
||||
pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay,
|
||||
width: displays[0].width,
|
||||
height: displays[0].height,
|
||||
);
|
||||
@@ -1100,6 +1180,14 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
void _queryAuditGuid(String peerId) async {
|
||||
try {
|
||||
if (bind.isDisableAccount()) {
|
||||
return;
|
||||
}
|
||||
if (bind
|
||||
.sessionGetAuditServerSync(sessionId: sessionId, typ: "conn/active")
|
||||
.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!mainGetLocalBoolOptionSync(
|
||||
kOptionAllowAskForNoteAtEndOfConnection)) {
|
||||
return;
|
||||
@@ -1183,9 +1271,6 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
_queryAuditGuid(peerId);
|
||||
|
||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
|
||||
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
||||
// Because this function is asynchronous, there's an "await" in this function.
|
||||
cachedPeerData.peerInfo = {...evt};
|
||||
@@ -1197,6 +1282,17 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
parent.target?.dialogManager.dismissAll();
|
||||
_pi.version = evt['version'];
|
||||
// Note: Relative mouse mode is NOT auto-enabled on connect.
|
||||
// Users must manually enable it via toolbar or keyboard shortcut (Ctrl+Alt+Shift+M).
|
||||
//
|
||||
// For desktop/webDesktop, keyboard mode initialization is handled later by
|
||||
// checkDesktopKeyboardMode() which may change the mode if not supported,
|
||||
// followed by updateKeyboardMode() to sync InputModel.keyboardMode.
|
||||
// For mobile, updateKeyboardMode() is currently a no-op (only executes on desktop/web),
|
||||
// but we call it here for consistency and future-proofing.
|
||||
if (isMobile) {
|
||||
parent.target?.inputModel.updateKeyboardMode();
|
||||
}
|
||||
_pi.isSupportMultiUiSession =
|
||||
bind.isSupportMultiUiSession(version: _pi.version);
|
||||
_pi.username = evt['username'];
|
||||
@@ -1265,6 +1361,7 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
_reconnects = 1;
|
||||
_offlineReconnectStartTime = null;
|
||||
waitForFirstImage.value = true;
|
||||
isRefreshing = false;
|
||||
}
|
||||
@@ -1298,7 +1395,11 @@ class FfiModel with ChangeNotifier {
|
||||
stateGlobal.resetLastResolutionGroupValues(peerId);
|
||||
|
||||
if (isDesktop || isWebDesktop) {
|
||||
checkDesktopKeyboardMode();
|
||||
// checkDesktopKeyboardMode may change the keyboard mode if the current
|
||||
// mode is not supported. Re-sync InputModel.keyboardMode afterwards.
|
||||
// Note: updateKeyboardMode() is a no-op on mobile (early-returns).
|
||||
await checkDesktopKeyboardMode();
|
||||
await parent.target?.inputModel.updateKeyboardMode();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
@@ -3759,6 +3860,8 @@ class FFI {
|
||||
ffiModel.clear();
|
||||
canvasModel.clear();
|
||||
inputModel.resetModifiers();
|
||||
// Dispose relative mouse mode resources to ensure cursor is restored
|
||||
inputModel.disposeRelativeMouseMode();
|
||||
if (closeSession) {
|
||||
await bind.sessionClose(sessionId: sessionId);
|
||||
}
|
||||
|
||||
1061
flutter/lib/models/relative_mouse_model.dart
Normal file
1061
flutter/lib/models/relative_mouse_model.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../common.dart';
|
||||
@@ -51,6 +50,8 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
Timer? cmHiddenTimer;
|
||||
|
||||
final _wakelockKey = UniqueKey();
|
||||
|
||||
bool get isStart => _isStart;
|
||||
|
||||
bool get mediaOk => _mediaOk;
|
||||
@@ -466,10 +467,8 @@ class ServerModel with ChangeNotifier {
|
||||
await parent.target?.invokeMethod("stop_service");
|
||||
await bind.mainStopService();
|
||||
notifyListeners();
|
||||
if (!isLinux) {
|
||||
// current linux is not supported
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
// for androidUpdatekeepScreenOn only
|
||||
WakelockManager.disable(_wakelockKey);
|
||||
}
|
||||
|
||||
Future<bool> setPermanentPassword(String newPW) async {
|
||||
@@ -613,12 +612,12 @@ class ServerModel with ChangeNotifier {
|
||||
void showLoginDialog(Client client) {
|
||||
showClientDialog(
|
||||
client,
|
||||
client.isFileTransfer
|
||||
? "Transfer file"
|
||||
client.isFileTransfer
|
||||
? "Transfer file"
|
||||
: client.isViewCamera
|
||||
? "View camera"
|
||||
: client.isTerminal
|
||||
? "Terminal"
|
||||
: client.isTerminal
|
||||
? "Terminal"
|
||||
: "Share screen",
|
||||
'Do you accept?',
|
||||
'android_new_connection_tip',
|
||||
@@ -797,12 +796,10 @@ class ServerModel with ChangeNotifier {
|
||||
final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
|
||||
(keepScreenOn == KeepScreenOn.duringControlled &&
|
||||
_clients.map((e) => !e.disconnected).isNotEmpty);
|
||||
if (on != await WakelockPlus.enabled) {
|
||||
if (on) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
if (on) {
|
||||
WakelockManager.enable(_wakelockKey, isServer: true);
|
||||
} else {
|
||||
WakelockManager.disable(_wakelockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -30,6 +29,11 @@ class StateGlobal {
|
||||
|
||||
String _inputSource = '';
|
||||
|
||||
// Track relative mouse mode state for each peer connection.
|
||||
// Key: peerId, Value: true if relative mouse mode is active.
|
||||
// Note: This is session-only runtime state, NOT persisted to config.
|
||||
final RxMap<String, bool> relativeMouseModeState = <String, bool>{}.obs;
|
||||
|
||||
// Use for desktop -> remote toolbar -> resolution
|
||||
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
|
||||
|
||||
|
||||
@@ -146,6 +146,10 @@ class TerminalModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendVirtualKey(String data) async {
|
||||
return _handleInput(data);
|
||||
}
|
||||
|
||||
Future<void> closeTerminal() async {
|
||||
if (_terminalOpened) {
|
||||
try {
|
||||
|
||||
58
flutter/lib/utils/relative_mouse_accumulator.dart
Normal file
58
flutter/lib/utils/relative_mouse_accumulator.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
/// A small helper for accumulating fractional mouse deltas and emitting integer deltas.
|
||||
///
|
||||
/// Relative mouse mode uses integer deltas on the wire, but Flutter pointer deltas
|
||||
/// are doubles. This accumulator preserves sub-pixel movement by carrying the
|
||||
/// fractional remainder across events.
|
||||
class RelativeMouseDelta {
|
||||
final int x;
|
||||
final int y;
|
||||
|
||||
const RelativeMouseDelta(this.x, this.y);
|
||||
}
|
||||
|
||||
/// Accumulates fractional mouse deltas and returns integer deltas when available.
|
||||
class RelativeMouseAccumulator {
|
||||
double _fracX = 0.0;
|
||||
double _fracY = 0.0;
|
||||
|
||||
/// Adds a delta and returns an integer delta when at least one axis reaches a
|
||||
/// magnitude of 1px (after truncation towards zero).
|
||||
///
|
||||
/// If [maxDelta] is > 0, the returned integer delta is clamped to
|
||||
/// [-maxDelta, maxDelta] on each axis.
|
||||
RelativeMouseDelta? add(
|
||||
double dx,
|
||||
double dy, {
|
||||
required int maxDelta,
|
||||
}) {
|
||||
// Guard against misuse: negative maxDelta would silently disable clamping.
|
||||
assert(maxDelta >= 0, 'maxDelta must be non-negative');
|
||||
|
||||
_fracX += dx;
|
||||
_fracY += dy;
|
||||
|
||||
int intX = _fracX.truncate();
|
||||
int intY = _fracY.truncate();
|
||||
|
||||
if (intX == 0 && intY == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Clamp before subtracting so excess movement is preserved in the accumulator
|
||||
// rather than being permanently discarded during spikes.
|
||||
if (maxDelta > 0) {
|
||||
intX = intX.clamp(-maxDelta, maxDelta);
|
||||
intY = intY.clamp(-maxDelta, maxDelta);
|
||||
}
|
||||
|
||||
_fracX -= intX;
|
||||
_fracY -= intY;
|
||||
|
||||
return RelativeMouseDelta(intX, intY);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_fracX = 0.0;
|
||||
_fracY = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -2020,5 +2020,19 @@ class RustdeskImpl {
|
||||
return js.context.callMethod('getByName', ['audit_guid']);
|
||||
}
|
||||
|
||||
bool mainSetCursorPosition({required int x, required int y, dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mainClipCursor(
|
||||
{required int left,
|
||||
required int top,
|
||||
required int right,
|
||||
required int bottom,
|
||||
required bool enable,
|
||||
dynamic hint}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,22 @@ import window_manager
|
||||
import window_size
|
||||
import texture_rgba_renderer
|
||||
|
||||
// Global state for relative mouse mode
|
||||
// All properties and methods must be accessed on the main thread since they
|
||||
// interact with NSEvent monitors, CoreGraphics APIs, and Flutter channels.
|
||||
// Note: We avoid @MainActor to maintain macOS 10.14 compatibility.
|
||||
class RelativeMouseState {
|
||||
static let shared = RelativeMouseState()
|
||||
|
||||
var enabled = false
|
||||
var eventMonitor: Any?
|
||||
var deltaChannel: FlutterMethodChannel?
|
||||
var accumulatedDeltaX: CGFloat = 0
|
||||
var accumulatedDeltaY: CGFloat = 0
|
||||
|
||||
private init() {}
|
||||
}
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
rustdesk_core_main();
|
||||
@@ -64,6 +80,104 @@ class MainFlutterWindow: NSWindow {
|
||||
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
||||
}
|
||||
|
||||
private func enableNativeRelativeMouseMode(channel: FlutterMethodChannel) -> Bool {
|
||||
assert(Thread.isMainThread, "enableNativeRelativeMouseMode must be called on the main thread")
|
||||
let state = RelativeMouseState.shared
|
||||
if state.enabled {
|
||||
// Already enabled: update the channel so this caller receives deltas.
|
||||
state.deltaChannel = channel
|
||||
return true
|
||||
}
|
||||
|
||||
// Dissociate mouse from cursor position - this locks the cursor in place
|
||||
// Do this FIRST before setting any state
|
||||
let result = CGAssociateMouseAndMouseCursorPosition(0)
|
||||
if result != CGError.success {
|
||||
NSLog("[RustDesk] Failed to dissociate mouse from cursor position: %d", result.rawValue)
|
||||
return false
|
||||
}
|
||||
|
||||
// Only set state after CG call succeeds
|
||||
state.deltaChannel = channel
|
||||
state.accumulatedDeltaX = 0
|
||||
state.accumulatedDeltaY = 0
|
||||
|
||||
// Add local event monitor to capture mouse delta.
|
||||
// Note: Local event monitors are always called on the main thread,
|
||||
// so accessing main-thread-only state is safe here.
|
||||
state.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak state] event in
|
||||
guard let state = state else { return event }
|
||||
// Guard against race: mode may be disabled between weak capture and this check.
|
||||
guard state.enabled else { return event }
|
||||
let deltaX = event.deltaX
|
||||
let deltaY = event.deltaY
|
||||
|
||||
if deltaX != 0 || deltaY != 0 {
|
||||
// Accumulate delta (main thread only - NSEvent local monitors always run on main thread)
|
||||
state.accumulatedDeltaX += deltaX
|
||||
state.accumulatedDeltaY += deltaY
|
||||
|
||||
// Only send if we have integer movement
|
||||
let intX = Int(state.accumulatedDeltaX)
|
||||
let intY = Int(state.accumulatedDeltaY)
|
||||
|
||||
if intX != 0 || intY != 0 {
|
||||
state.accumulatedDeltaX -= CGFloat(intX)
|
||||
state.accumulatedDeltaY -= CGFloat(intY)
|
||||
|
||||
// Send delta to Flutter (already on main thread)
|
||||
state.deltaChannel?.invokeMethod("onMouseDelta", arguments: ["dx": intX, "dy": intY])
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// Check if monitor was created successfully
|
||||
if state.eventMonitor == nil {
|
||||
NSLog("[RustDesk] Failed to create event monitor for relative mouse mode")
|
||||
// Re-associate mouse since we failed
|
||||
CGAssociateMouseAndMouseCursorPosition(1)
|
||||
state.deltaChannel = nil
|
||||
return false
|
||||
}
|
||||
|
||||
// Set enabled LAST after everything succeeds
|
||||
state.enabled = true
|
||||
return true
|
||||
}
|
||||
|
||||
private func disableNativeRelativeMouseMode() {
|
||||
assert(Thread.isMainThread, "disableNativeRelativeMouseMode must be called on the main thread")
|
||||
let state = RelativeMouseState.shared
|
||||
if !state.enabled { return }
|
||||
|
||||
state.enabled = false
|
||||
|
||||
// Remove event monitor
|
||||
if let monitor = state.eventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
state.eventMonitor = nil
|
||||
}
|
||||
|
||||
state.deltaChannel = nil
|
||||
state.accumulatedDeltaX = 0
|
||||
state.accumulatedDeltaY = 0
|
||||
|
||||
// Re-associate mouse with cursor position (non-blocking with async retry)
|
||||
let result = CGAssociateMouseAndMouseCursorPosition(1)
|
||||
if result != CGError.success {
|
||||
NSLog("[RustDesk] Failed to re-associate mouse with cursor position: %d, scheduling retry...", result.rawValue)
|
||||
// Non-blocking retry after 50ms
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
let retryResult = CGAssociateMouseAndMouseCursorPosition(1)
|
||||
if retryResult != CGError.success {
|
||||
NSLog("[RustDesk] Retry failed to re-associate mouse: %d. Cursor may remain locked.", retryResult.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
||||
channel.setMethodCallHandler({
|
||||
@@ -95,8 +209,42 @@ class MainFlutterWindow: NSWindow {
|
||||
break
|
||||
}
|
||||
case "requestRecordAudio":
|
||||
// Request microphone access and trigger system registration
|
||||
// On macOS 13+, apps only appear in System Settings > Privacy & Security > Microphone
|
||||
// after they actually attempt to use the microphone, not just request permission.
|
||||
// We create a brief capture session to ensure proper registration.
|
||||
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
|
||||
result(granted)
|
||||
if granted {
|
||||
// Instantiate an audio capture session to trigger macOS registration
|
||||
// This needs to run on main thread to ensure proper lifecycle
|
||||
DispatchQueue.main.async {
|
||||
if let audioDevice = AVCaptureDevice.default(for: .audio) {
|
||||
do {
|
||||
let audioInput = try AVCaptureDeviceInput(device: audioDevice)
|
||||
let captureSession = AVCaptureSession()
|
||||
captureSession.beginConfiguration()
|
||||
if captureSession.canAddInput(audioInput) {
|
||||
captureSession.addInput(audioInput)
|
||||
}
|
||||
captureSession.commitConfiguration()
|
||||
// Start and immediately stop the session to trigger registration
|
||||
captureSession.startRunning()
|
||||
// Minimum delay required for macOS to register the app in System Settings
|
||||
let registrationDelay: TimeInterval = 0.1
|
||||
// Keep a strong reference and stop after the registration delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + registrationDelay) { [captureSession] in
|
||||
captureSession.stopRunning()
|
||||
}
|
||||
} catch {
|
||||
NSLog("[RustDesk] Failed to create audio capture session: %@", error.localizedDescription)
|
||||
NSLog("[RustDesk] The app may not appear in System Settings > Privacy & Security > Microphone")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
result(granted)
|
||||
}
|
||||
})
|
||||
break
|
||||
case "bumpMouse":
|
||||
@@ -145,11 +293,22 @@ class MainFlutterWindow: NSWindow {
|
||||
// This function's main action is to toggle whether the mouse cursor is
|
||||
// associated with the mouse position, but setting it to true when it's
|
||||
// already true has the side-effect of cancelling this motion suppression.
|
||||
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
||||
//
|
||||
// However, we must NOT call this when relative mouse mode is active,
|
||||
// as it would break the pointer lock established by enableNativeRelativeMouseMode.
|
||||
if !RelativeMouseState.shared.enabled {
|
||||
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
||||
}
|
||||
|
||||
result(true)
|
||||
|
||||
break
|
||||
case "enableNativeRelativeMouseMode":
|
||||
let success = self.enableNativeRelativeMouseMode(channel: channel)
|
||||
result(success)
|
||||
|
||||
case "disableNativeRelativeMouseMode":
|
||||
self.disableNativeRelativeMouseMode()
|
||||
result(true)
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Fix OpenSSL build with Android NDK clang on 32-bit architectures
|
||||
#
|
||||
|
||||
export CFLAGS="-DBROKEN_CLANG_ATOMICS"
|
||||
export CXXFLAGS="-DBROKEN_CLANG_ATOMICS"
|
||||
|
||||
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.4.4+62
|
||||
version: 1.4.5+63
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -37,5 +37,8 @@ core-graphics = "0.22"
|
||||
objc = "0.2"
|
||||
unicode-segmentation = "1.10"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libxdo-sys = "0.11"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3"
|
||||
|
||||
@@ -1,50 +1,22 @@
|
||||
//! XDO-based input emulation for Linux.
|
||||
//!
|
||||
//! This module uses libxdo-sys (patched to use dynamic loading stub) for input emulation.
|
||||
//! The stub handles dynamic loading of libxdo, so we just call the functions directly.
|
||||
//!
|
||||
//! If libxdo is not available at runtime, all operations become no-ops.
|
||||
|
||||
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
||||
|
||||
use hbb_common::libc::{c_char, c_int, c_void, useconds_t};
|
||||
use std::{borrow::Cow, ffi::CString, ptr};
|
||||
use hbb_common::libc::c_int;
|
||||
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
|
||||
use std::{borrow::Cow, ffi::CString};
|
||||
|
||||
const CURRENT_WINDOW: c_int = 0;
|
||||
/// Default delay per keypress in microseconds.
|
||||
/// This value is passed to libxdo functions and must fit in `useconds_t` (u32).
|
||||
const DEFAULT_DELAY: u64 = 12000;
|
||||
type Window = c_int;
|
||||
type Xdo = *const c_void;
|
||||
|
||||
#[link(name = "xdo")]
|
||||
extern "C" {
|
||||
fn xdo_free(xdo: Xdo);
|
||||
fn xdo_new(display: *const c_char) -> Xdo;
|
||||
|
||||
fn xdo_click_window(xdo: Xdo, window: Window, button: c_int) -> c_int;
|
||||
fn xdo_mouse_down(xdo: Xdo, window: Window, button: c_int) -> c_int;
|
||||
fn xdo_mouse_up(xdo: Xdo, window: Window, button: c_int) -> c_int;
|
||||
fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int;
|
||||
fn xdo_move_mouse_relative(xdo: Xdo, x: c_int, y: c_int) -> c_int;
|
||||
|
||||
fn xdo_enter_text_window(
|
||||
xdo: Xdo,
|
||||
window: Window,
|
||||
string: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int;
|
||||
fn xdo_send_keysequence_window(
|
||||
xdo: Xdo,
|
||||
window: Window,
|
||||
string: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int;
|
||||
fn xdo_send_keysequence_window_down(
|
||||
xdo: Xdo,
|
||||
window: Window,
|
||||
string: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int;
|
||||
fn xdo_send_keysequence_window_up(
|
||||
xdo: Xdo,
|
||||
window: Window,
|
||||
string: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int;
|
||||
fn xdo_get_input_state(xdo: Xdo) -> u32;
|
||||
}
|
||||
/// Maximum allowed delay value (u32::MAX as u64).
|
||||
const MAX_DELAY: u64 = u32::MAX as u64;
|
||||
|
||||
fn mousebutton(button: MouseButton) -> c_int {
|
||||
match button {
|
||||
@@ -62,7 +34,7 @@ fn mousebutton(button: MouseButton) -> c_int {
|
||||
|
||||
/// The main struct for handling the event emitting
|
||||
pub(super) struct EnigoXdo {
|
||||
xdo: Xdo,
|
||||
xdo: *mut xdo_t,
|
||||
delay: u64,
|
||||
}
|
||||
// This is safe, we have a unique pointer.
|
||||
@@ -70,37 +42,61 @@ pub(super) struct EnigoXdo {
|
||||
unsafe impl Send for EnigoXdo {}
|
||||
|
||||
impl Default for EnigoXdo {
|
||||
/// Create a new EnigoXdo instance
|
||||
/// Create a new EnigoXdo instance.
|
||||
///
|
||||
/// If libxdo is not available, the xdo pointer will be null and all
|
||||
/// input operations will be no-ops.
|
||||
fn default() -> Self {
|
||||
let xdo = unsafe { libxdo_sys::xdo_new(std::ptr::null()) };
|
||||
if xdo.is_null() {
|
||||
log::warn!("Failed to create xdo context, xdo functions will be disabled");
|
||||
} else {
|
||||
log::info!("xdo context created successfully");
|
||||
}
|
||||
Self {
|
||||
xdo: unsafe { xdo_new(ptr::null()) },
|
||||
xdo,
|
||||
delay: DEFAULT_DELAY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EnigoXdo {
|
||||
/// Get the delay per keypress.
|
||||
/// Default value is 12000.
|
||||
/// This is Linux-specific.
|
||||
/// Get the delay per keypress in microseconds.
|
||||
///
|
||||
/// Default value is 12000 (12ms). This is Linux-specific.
|
||||
pub fn delay(&self) -> u64 {
|
||||
self.delay
|
||||
}
|
||||
/// Set the delay per keypress.
|
||||
/// This is Linux-specific.
|
||||
|
||||
/// Set the delay per keypress in microseconds.
|
||||
///
|
||||
/// This is Linux-specific. The value is clamped to `u32::MAX` (approximately
|
||||
/// 4295 seconds) because libxdo uses `useconds_t` which is typically `u32`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `delay` - Delay in microseconds. Values exceeding `u32::MAX` will be clamped.
|
||||
pub fn set_delay(&mut self, delay: u64) {
|
||||
self.delay = delay;
|
||||
self.delay = delay.min(MAX_DELAY);
|
||||
if delay > MAX_DELAY {
|
||||
log::warn!(
|
||||
"delay value {} exceeds maximum {}, clamped",
|
||||
delay,
|
||||
MAX_DELAY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnigoXdo {
|
||||
fn drop(&mut self) {
|
||||
if self.xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
xdo_free(self.xdo);
|
||||
if !self.xdo.is_null() {
|
||||
unsafe {
|
||||
libxdo_sys::xdo_free(self.xdo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseControllable for EnigoXdo {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
@@ -115,42 +111,47 @@ impl MouseControllable for EnigoXdo {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
xdo_move_mouse(self.xdo, x as c_int, y as c_int, 0);
|
||||
libxdo_sys::xdo_move_mouse(self.xdo as *const _, x, y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
||||
if self.xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
xdo_move_mouse_relative(self.xdo, x as c_int, y as c_int);
|
||||
libxdo_sys::xdo_move_mouse_relative(self.xdo as *const _, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
|
||||
if self.xdo.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
unsafe {
|
||||
xdo_mouse_down(self.xdo, CURRENT_WINDOW, mousebutton(button));
|
||||
libxdo_sys::xdo_mouse_down(self.xdo as *const _, CURRENTWINDOW, mousebutton(button));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mouse_up(&mut self, button: MouseButton) {
|
||||
if self.xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
xdo_mouse_up(self.xdo, CURRENT_WINDOW, mousebutton(button));
|
||||
libxdo_sys::xdo_mouse_up(self.xdo as *const _, CURRENTWINDOW, mousebutton(button));
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_click(&mut self, button: MouseButton) {
|
||||
if self.xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
xdo_click_window(self.xdo, CURRENT_WINDOW, mousebutton(button));
|
||||
libxdo_sys::xdo_click_window(self.xdo as *const _, CURRENTWINDOW, mousebutton(button));
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_scroll_x(&mut self, length: i32) {
|
||||
let button;
|
||||
let mut length = length;
|
||||
@@ -169,6 +170,7 @@ impl MouseControllable for EnigoXdo {
|
||||
self.mouse_click(button);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_scroll_y(&mut self, length: i32) {
|
||||
let button;
|
||||
let mut length = length;
|
||||
@@ -188,6 +190,7 @@ impl MouseControllable for EnigoXdo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keysequence<'a>(key: Key) -> Cow<'a, str> {
|
||||
if let Key::Layout(c) = key {
|
||||
return Cow::Owned(format!("U{:X}", c as u32));
|
||||
@@ -284,6 +287,7 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> {
|
||||
_ => "",
|
||||
})
|
||||
}
|
||||
|
||||
impl KeyboardControllable for EnigoXdo {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
@@ -314,7 +318,7 @@ impl KeyboardControllable for EnigoXdo {
|
||||
let mod_alt = 1 << 3;
|
||||
let mod_numlock = 1 << 4;
|
||||
let mod_meta = 1 << 6;
|
||||
let mask = unsafe { xdo_get_input_state(self.xdo) };
|
||||
let mask = unsafe { libxdo_sys::xdo_get_input_state(self.xdo as *const _) };
|
||||
match key {
|
||||
Key::Shift => mask & mod_shift != 0,
|
||||
Key::CapsLock => mask & mod_lock != 0,
|
||||
@@ -332,56 +336,59 @@ impl KeyboardControllable for EnigoXdo {
|
||||
}
|
||||
if let Ok(string) = CString::new(sequence) {
|
||||
unsafe {
|
||||
xdo_enter_text_window(
|
||||
self.xdo,
|
||||
CURRENT_WINDOW,
|
||||
libxdo_sys::xdo_enter_text_window(
|
||||
self.xdo as *const _,
|
||||
CURRENTWINDOW,
|
||||
string.as_ptr(),
|
||||
self.delay as useconds_t,
|
||||
self.delay as libxdo_sys::useconds_t,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
||||
if self.xdo.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
let string = CString::new(&*keysequence(key))?;
|
||||
unsafe {
|
||||
xdo_send_keysequence_window_down(
|
||||
self.xdo,
|
||||
CURRENT_WINDOW,
|
||||
libxdo_sys::xdo_send_keysequence_window_down(
|
||||
self.xdo as *const _,
|
||||
CURRENTWINDOW,
|
||||
string.as_ptr(),
|
||||
self.delay as useconds_t,
|
||||
self.delay as libxdo_sys::useconds_t,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn key_up(&mut self, key: Key) {
|
||||
if self.xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(string) = CString::new(&*keysequence(key)) {
|
||||
unsafe {
|
||||
xdo_send_keysequence_window_up(
|
||||
self.xdo,
|
||||
CURRENT_WINDOW,
|
||||
libxdo_sys::xdo_send_keysequence_window_up(
|
||||
self.xdo as *const _,
|
||||
CURRENTWINDOW,
|
||||
string.as_ptr(),
|
||||
self.delay as useconds_t,
|
||||
self.delay as libxdo_sys::useconds_t,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_click(&mut self, key: Key) {
|
||||
if self.xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(string) = CString::new(&*keysequence(key)) {
|
||||
unsafe {
|
||||
xdo_send_keysequence_window(
|
||||
self.xdo,
|
||||
CURRENT_WINDOW,
|
||||
libxdo_sys::xdo_send_keysequence_window(
|
||||
self.xdo as *const _,
|
||||
CURRENTWINDOW,
|
||||
string.as_ptr(),
|
||||
self.delay as useconds_t,
|
||||
self.delay as libxdo_sys::useconds_t,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,42 +208,56 @@ impl MouseControllable for Enigo {
|
||||
}
|
||||
|
||||
fn mouse_move_to(&mut self, x: i32, y: i32) {
|
||||
let pressed = Self::pressed_buttons();
|
||||
|
||||
let event_type = if pressed & 1 > 0 {
|
||||
CGEventType::LeftMouseDragged
|
||||
} else if pressed & 2 > 0 {
|
||||
CGEventType::RightMouseDragged
|
||||
} else {
|
||||
CGEventType::MouseMoved
|
||||
};
|
||||
|
||||
let dest = CGPoint::new(x as f64, y as f64);
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
if let Ok(event) =
|
||||
CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left)
|
||||
{
|
||||
self.post(event, None);
|
||||
}
|
||||
}
|
||||
// For absolute movement, we don't set delta values
|
||||
// This maintains backward compatibility
|
||||
self.mouse_move_to_impl(x, y, None);
|
||||
}
|
||||
|
||||
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
||||
let (display_width, display_height) = Self::main_display_size();
|
||||
let (current_x, y_inv) = Self::mouse_location_raw_coords();
|
||||
let current_y = (display_height as i32) - y_inv;
|
||||
let new_x = current_x + x;
|
||||
let new_y = current_y + y;
|
||||
// Use saturating arithmetic to prevent overflow/wraparound
|
||||
let mut new_x = current_x.saturating_add(x);
|
||||
let mut new_y = current_y.saturating_add(y);
|
||||
|
||||
if new_x < 0
|
||||
|| new_x as usize > display_width
|
||||
|| new_y < 0
|
||||
|| new_y as usize > display_height
|
||||
{
|
||||
return;
|
||||
// Define screen center and edge margins for cursor reset
|
||||
let center_x = (display_width / 2) as i32;
|
||||
let center_y = (display_height / 2) as i32;
|
||||
// Margin calculation: 5% of the smaller screen dimension with a minimum of 50px.
|
||||
// This provides a comfortable buffer zone to detect when the cursor is approaching
|
||||
// screen edges, allowing us to reset it to center before it hits the boundary.
|
||||
// This ensures continuous relative mouse movement without getting stuck at edges.
|
||||
let margin = (display_width.min(display_height) / 20).max(50) as i32;
|
||||
|
||||
// Check if cursor is approaching screen boundaries
|
||||
// Use saturating_sub to prevent negative thresholds on very small displays
|
||||
let right = (display_width as i32).saturating_sub(margin);
|
||||
let bottom = (display_height as i32).saturating_sub(margin);
|
||||
let near_edge = new_x < margin
|
||||
|| new_x > right
|
||||
|| new_y < margin
|
||||
|| new_y > bottom;
|
||||
|
||||
if near_edge {
|
||||
// Reset cursor to screen center to allow continuous movement
|
||||
// The delta values are still passed correctly for games/apps
|
||||
new_x = center_x;
|
||||
new_y = center_y;
|
||||
}
|
||||
|
||||
self.mouse_move_to(new_x, new_y);
|
||||
// Clamp to screen bounds as a safety measure.
|
||||
// Use saturating_sub(1) to ensure coordinates don't exceed the last valid pixel.
|
||||
let max_x = (display_width as i32).saturating_sub(1).max(0);
|
||||
let max_y = (display_height as i32).saturating_sub(1).max(0);
|
||||
new_x = new_x.clamp(0, max_x);
|
||||
new_y = new_y.clamp(0, max_y);
|
||||
|
||||
// Pass delta values for relative movement
|
||||
// This is critical for browser Pointer Lock API support
|
||||
// The delta fields (MOUSE_EVENT_DELTA_X/Y) are used by browsers
|
||||
// to calculate movementX/Y in Pointer Lock mode
|
||||
self.mouse_move_to_impl(new_x, new_y, Some((x, y)));
|
||||
}
|
||||
|
||||
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
|
||||
@@ -473,6 +487,43 @@ impl Enigo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal implementation for mouse movement with optional delta values.
|
||||
///
|
||||
/// The `delta` parameter is crucial for browser Pointer Lock API support.
|
||||
/// When a browser enters Pointer Lock mode, it reads mouse delta values
|
||||
/// (MOUSE_EVENT_DELTA_X/Y) directly from CGEvent to calculate movementX/Y.
|
||||
/// Without setting these fields, the browser sees zero movement.
|
||||
fn mouse_move_to_impl(&mut self, x: i32, y: i32, delta: Option<(i32, i32)>) {
|
||||
let pressed = Self::pressed_buttons();
|
||||
|
||||
// Determine event type and corresponding mouse button based on pressed buttons.
|
||||
// The CGMouseButton must match the event type for drag events.
|
||||
let (event_type, button) = if pressed & 1 > 0 {
|
||||
(CGEventType::LeftMouseDragged, CGMouseButton::Left)
|
||||
} else if pressed & 2 > 0 {
|
||||
(CGEventType::RightMouseDragged, CGMouseButton::Right)
|
||||
} else if pressed & 4 > 0 {
|
||||
(CGEventType::OtherMouseDragged, CGMouseButton::Center)
|
||||
} else {
|
||||
(CGEventType::MouseMoved, CGMouseButton::Left) // Button doesn't matter for MouseMoved
|
||||
};
|
||||
|
||||
let dest = CGPoint::new(x as f64, y as f64);
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
if let Ok(event) =
|
||||
CGEvent::new_mouse_event(src.clone(), event_type, dest, button)
|
||||
{
|
||||
// Set delta fields for relative mouse movement
|
||||
// This is essential for Pointer Lock API in browsers
|
||||
if let Some((dx, dy)) = delta {
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
|
||||
}
|
||||
self.post(event, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the `(width, height)` in pixels of the main display
|
||||
pub fn main_display_size() -> (usize, usize) {
|
||||
let display_id = unsafe { CGMainDisplayID() };
|
||||
|
||||
Submodule libs/hbb_common updated: a86eda749e...900077a2c2
9
libs/libxdo-sys-stub/Cargo.toml
Normal file
9
libs/libxdo-sys-stub/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "libxdo-sys"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
description = "Dynamic loading wrapper for libxdo-sys that doesn't require libxdo at compile/link time"
|
||||
|
||||
[dependencies]
|
||||
hbb_common = { path = "../hbb_common" }
|
||||
505
libs/libxdo-sys-stub/src/lib.rs
Normal file
505
libs/libxdo-sys-stub/src/lib.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
//! Dynamic loading wrapper for libxdo.
|
||||
//!
|
||||
//! Provides the same API as libxdo-sys but loads libxdo at runtime,
|
||||
//! allowing the program to run on systems without libxdo installed
|
||||
//! (e.g., Wayland-only environments).
|
||||
|
||||
use hbb_common::{
|
||||
libc::{c_char, c_int, c_uint},
|
||||
libloading::{Library, Symbol},
|
||||
log,
|
||||
};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use hbb_common::x11::xlib::{Display, Screen, Window};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xdo_t {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct charcodemap_t {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct xdo_search_t {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
pub type useconds_t = c_uint;
|
||||
|
||||
pub const CURRENTWINDOW: Window = 0;
|
||||
|
||||
type FnXdoNew = unsafe extern "C" fn(*const c_char) -> *mut xdo_t;
|
||||
type FnXdoNewWithOpenedDisplay =
|
||||
unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> *mut xdo_t;
|
||||
type FnXdoFree = unsafe extern "C" fn(*mut xdo_t);
|
||||
type FnXdoSendKeysequenceWindow =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int;
|
||||
type FnXdoSendKeysequenceWindowDown =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int;
|
||||
type FnXdoSendKeysequenceWindowUp =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int;
|
||||
type FnXdoEnterTextWindow =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int;
|
||||
type FnXdoClickWindow = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int;
|
||||
type FnXdoMouseDown = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int;
|
||||
type FnXdoMouseUp = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int;
|
||||
type FnXdoMoveMouse = unsafe extern "C" fn(*const xdo_t, c_int, c_int, c_int) -> c_int;
|
||||
type FnXdoMoveMouseRelative = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int;
|
||||
type FnXdoMoveMouseRelativeToWindow =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, c_int, c_int) -> c_int;
|
||||
type FnXdoGetMouseLocation =
|
||||
unsafe extern "C" fn(*const xdo_t, *mut c_int, *mut c_int, *mut c_int) -> c_int;
|
||||
type FnXdoGetMouseLocation2 =
|
||||
unsafe extern "C" fn(*const xdo_t, *mut c_int, *mut c_int, *mut c_int, *mut Window) -> c_int;
|
||||
type FnXdoGetActiveWindow = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int;
|
||||
type FnXdoGetFocusedWindow = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int;
|
||||
type FnXdoGetFocusedWindowSane = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int;
|
||||
type FnXdoGetWindowLocation =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *mut c_int, *mut c_int, *mut *mut Screen) -> c_int;
|
||||
type FnXdoGetWindowSize =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *mut c_uint, *mut c_uint) -> c_int;
|
||||
type FnXdoGetInputState = unsafe extern "C" fn(*const xdo_t) -> c_uint;
|
||||
type FnXdoActivateWindow = unsafe extern "C" fn(*const xdo_t, Window) -> c_int;
|
||||
type FnXdoWaitForMouseMoveFrom = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int;
|
||||
type FnXdoWaitForMouseMoveTo = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int;
|
||||
type FnXdoSetWindowClass =
|
||||
unsafe extern "C" fn(*const xdo_t, Window, *const c_char, *const c_char) -> c_int;
|
||||
type FnXdoSearchWindows =
|
||||
unsafe extern "C" fn(*const xdo_t, *const xdo_search_t, *mut *mut Window, *mut c_uint) -> c_int;
|
||||
|
||||
struct XdoLib {
|
||||
_lib: Library,
|
||||
xdo_new: FnXdoNew,
|
||||
xdo_new_with_opened_display: Option<FnXdoNewWithOpenedDisplay>,
|
||||
xdo_free: FnXdoFree,
|
||||
xdo_send_keysequence_window: FnXdoSendKeysequenceWindow,
|
||||
xdo_send_keysequence_window_down: Option<FnXdoSendKeysequenceWindowDown>,
|
||||
xdo_send_keysequence_window_up: Option<FnXdoSendKeysequenceWindowUp>,
|
||||
xdo_enter_text_window: Option<FnXdoEnterTextWindow>,
|
||||
xdo_click_window: Option<FnXdoClickWindow>,
|
||||
xdo_mouse_down: Option<FnXdoMouseDown>,
|
||||
xdo_mouse_up: Option<FnXdoMouseUp>,
|
||||
xdo_move_mouse: Option<FnXdoMoveMouse>,
|
||||
xdo_move_mouse_relative: Option<FnXdoMoveMouseRelative>,
|
||||
xdo_move_mouse_relative_to_window: Option<FnXdoMoveMouseRelativeToWindow>,
|
||||
xdo_get_mouse_location: Option<FnXdoGetMouseLocation>,
|
||||
xdo_get_mouse_location2: Option<FnXdoGetMouseLocation2>,
|
||||
xdo_get_active_window: Option<FnXdoGetActiveWindow>,
|
||||
xdo_get_focused_window: Option<FnXdoGetFocusedWindow>,
|
||||
xdo_get_focused_window_sane: Option<FnXdoGetFocusedWindowSane>,
|
||||
xdo_get_window_location: Option<FnXdoGetWindowLocation>,
|
||||
xdo_get_window_size: Option<FnXdoGetWindowSize>,
|
||||
xdo_get_input_state: Option<FnXdoGetInputState>,
|
||||
xdo_activate_window: Option<FnXdoActivateWindow>,
|
||||
xdo_wait_for_mouse_move_from: Option<FnXdoWaitForMouseMoveFrom>,
|
||||
xdo_wait_for_mouse_move_to: Option<FnXdoWaitForMouseMoveTo>,
|
||||
xdo_set_window_class: Option<FnXdoSetWindowClass>,
|
||||
xdo_search_windows: Option<FnXdoSearchWindows>,
|
||||
}
|
||||
|
||||
impl XdoLib {
|
||||
fn load() -> Option<Self> {
|
||||
// https://github.com/rustdesk/rustdesk/issues/13711
|
||||
const LIB_NAMES: [&str; 3] = ["libxdo.so.4", "libxdo.so.3", "libxdo.so"];
|
||||
|
||||
unsafe {
|
||||
let (lib, lib_name) = LIB_NAMES
|
||||
.iter()
|
||||
.find_map(|name| Library::new(name).ok().map(|lib| (lib, *name)))?;
|
||||
|
||||
log::info!("libxdo-sys Loaded {}", lib_name);
|
||||
|
||||
let xdo_new: FnXdoNew = *lib.get(b"xdo_new").ok()?;
|
||||
let xdo_free: FnXdoFree = *lib.get(b"xdo_free").ok()?;
|
||||
let xdo_send_keysequence_window: FnXdoSendKeysequenceWindow =
|
||||
*lib.get(b"xdo_send_keysequence_window").ok()?;
|
||||
|
||||
let xdo_new_with_opened_display = lib
|
||||
.get(b"xdo_new_with_opened_display")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoNewWithOpenedDisplay>| *s);
|
||||
let xdo_send_keysequence_window_down = lib
|
||||
.get(b"xdo_send_keysequence_window_down")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoSendKeysequenceWindowDown>| *s);
|
||||
let xdo_send_keysequence_window_up = lib
|
||||
.get(b"xdo_send_keysequence_window_up")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoSendKeysequenceWindowUp>| *s);
|
||||
let xdo_enter_text_window = lib
|
||||
.get(b"xdo_enter_text_window")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoEnterTextWindow>| *s);
|
||||
let xdo_click_window = lib
|
||||
.get(b"xdo_click_window")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoClickWindow>| *s);
|
||||
let xdo_mouse_down = lib
|
||||
.get(b"xdo_mouse_down")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoMouseDown>| *s);
|
||||
let xdo_mouse_up = lib
|
||||
.get(b"xdo_mouse_up")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoMouseUp>| *s);
|
||||
let xdo_move_mouse = lib
|
||||
.get(b"xdo_move_mouse")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoMoveMouse>| *s);
|
||||
let xdo_move_mouse_relative = lib
|
||||
.get(b"xdo_move_mouse_relative")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoMoveMouseRelative>| *s);
|
||||
let xdo_move_mouse_relative_to_window = lib
|
||||
.get(b"xdo_move_mouse_relative_to_window")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoMoveMouseRelativeToWindow>| *s);
|
||||
let xdo_get_mouse_location = lib
|
||||
.get(b"xdo_get_mouse_location")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetMouseLocation>| *s);
|
||||
let xdo_get_mouse_location2 = lib
|
||||
.get(b"xdo_get_mouse_location2")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetMouseLocation2>| *s);
|
||||
let xdo_get_active_window = lib
|
||||
.get(b"xdo_get_active_window")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetActiveWindow>| *s);
|
||||
let xdo_get_focused_window = lib
|
||||
.get(b"xdo_get_focused_window")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetFocusedWindow>| *s);
|
||||
let xdo_get_focused_window_sane = lib
|
||||
.get(b"xdo_get_focused_window_sane")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetFocusedWindowSane>| *s);
|
||||
let xdo_get_window_location = lib
|
||||
.get(b"xdo_get_window_location")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetWindowLocation>| *s);
|
||||
let xdo_get_window_size = lib
|
||||
.get(b"xdo_get_window_size")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetWindowSize>| *s);
|
||||
let xdo_get_input_state = lib
|
||||
.get(b"xdo_get_input_state")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoGetInputState>| *s);
|
||||
let xdo_activate_window = lib
|
||||
.get(b"xdo_activate_window")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoActivateWindow>| *s);
|
||||
let xdo_wait_for_mouse_move_from = lib
|
||||
.get(b"xdo_wait_for_mouse_move_from")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoWaitForMouseMoveFrom>| *s);
|
||||
let xdo_wait_for_mouse_move_to = lib
|
||||
.get(b"xdo_wait_for_mouse_move_to")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoWaitForMouseMoveTo>| *s);
|
||||
let xdo_set_window_class = lib
|
||||
.get(b"xdo_set_window_class")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoSetWindowClass>| *s);
|
||||
let xdo_search_windows = lib
|
||||
.get(b"xdo_search_windows")
|
||||
.ok()
|
||||
.map(|s: Symbol<FnXdoSearchWindows>| *s);
|
||||
|
||||
Some(Self {
|
||||
_lib: lib,
|
||||
xdo_new,
|
||||
xdo_new_with_opened_display,
|
||||
xdo_free,
|
||||
xdo_send_keysequence_window,
|
||||
xdo_send_keysequence_window_down,
|
||||
xdo_send_keysequence_window_up,
|
||||
xdo_enter_text_window,
|
||||
xdo_click_window,
|
||||
xdo_mouse_down,
|
||||
xdo_mouse_up,
|
||||
xdo_move_mouse,
|
||||
xdo_move_mouse_relative,
|
||||
xdo_move_mouse_relative_to_window,
|
||||
xdo_get_mouse_location,
|
||||
xdo_get_mouse_location2,
|
||||
xdo_get_active_window,
|
||||
xdo_get_focused_window,
|
||||
xdo_get_focused_window_sane,
|
||||
xdo_get_window_location,
|
||||
xdo_get_window_size,
|
||||
xdo_get_input_state,
|
||||
xdo_activate_window,
|
||||
xdo_wait_for_mouse_move_from,
|
||||
xdo_wait_for_mouse_move_to,
|
||||
xdo_set_window_class,
|
||||
xdo_search_windows,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static XDO_LIB: OnceLock<Option<XdoLib>> = OnceLock::new();
|
||||
|
||||
fn get_lib() -> Option<&'static XdoLib> {
|
||||
XDO_LIB
|
||||
.get_or_init(|| {
|
||||
let lib = XdoLib::load();
|
||||
if lib.is_none() {
|
||||
log::info!("libxdo-sys libxdo not found, xdo functions will be disabled");
|
||||
}
|
||||
lib
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_new(display: *const c_char) -> *mut xdo_t {
|
||||
get_lib().map_or(std::ptr::null_mut(), |lib| (lib.xdo_new)(display))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_new_with_opened_display(
|
||||
xdpy: *mut Display,
|
||||
display: *const c_char,
|
||||
close_display_when_freed: c_int,
|
||||
) -> *mut xdo_t {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_new_with_opened_display)
|
||||
.map_or(std::ptr::null_mut(), |f| {
|
||||
f(xdpy, display, close_display_when_freed)
|
||||
})
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_free(xdo: *mut xdo_t) {
|
||||
if xdo.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(lib) = get_lib() {
|
||||
(lib.xdo_free)(xdo);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_send_keysequence_window(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
keysequence: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int {
|
||||
get_lib().map_or(1, |lib| {
|
||||
(lib.xdo_send_keysequence_window)(xdo, window, keysequence, delay)
|
||||
})
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_send_keysequence_window_down(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
keysequence: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_send_keysequence_window_down)
|
||||
.map_or(1, |f| f(xdo, window, keysequence, delay))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_send_keysequence_window_up(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
keysequence: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_send_keysequence_window_up)
|
||||
.map_or(1, |f| f(xdo, window, keysequence, delay))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_enter_text_window(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
string: *const c_char,
|
||||
delay: useconds_t,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_enter_text_window)
|
||||
.map_or(1, |f| f(xdo, window, string, delay))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_click_window(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
button: c_int,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_click_window)
|
||||
.map_or(1, |f| f(xdo, window, button))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_mouse_down(xdo: *const xdo_t, window: Window, button: c_int) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_mouse_down)
|
||||
.map_or(1, |f| f(xdo, window, button))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_mouse_up(xdo: *const xdo_t, window: Window, button: c_int) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_mouse_up)
|
||||
.map_or(1, |f| f(xdo, window, button))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_move_mouse(
|
||||
xdo: *const xdo_t,
|
||||
x: c_int,
|
||||
y: c_int,
|
||||
screen: c_int,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_move_mouse)
|
||||
.map_or(1, |f| f(xdo, x, y, screen))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_move_mouse_relative(xdo: *const xdo_t, x: c_int, y: c_int) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_move_mouse_relative)
|
||||
.map_or(1, |f| f(xdo, x, y))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_move_mouse_relative_to_window(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
x: c_int,
|
||||
y: c_int,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_move_mouse_relative_to_window)
|
||||
.map_or(1, |f| f(xdo, window, x, y))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_mouse_location(
|
||||
xdo: *const xdo_t,
|
||||
x: *mut c_int,
|
||||
y: *mut c_int,
|
||||
screen_num: *mut c_int,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_mouse_location)
|
||||
.map_or(1, |f| f(xdo, x, y, screen_num))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_mouse_location2(
|
||||
xdo: *const xdo_t,
|
||||
x: *mut c_int,
|
||||
y: *mut c_int,
|
||||
screen_num: *mut c_int,
|
||||
window: *mut Window,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_mouse_location2)
|
||||
.map_or(1, |f| f(xdo, x, y, screen_num, window))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_active_window(
|
||||
xdo: *const xdo_t,
|
||||
window_ret: *mut Window,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_active_window)
|
||||
.map_or(1, |f| f(xdo, window_ret))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_focused_window(
|
||||
xdo: *const xdo_t,
|
||||
window_ret: *mut Window,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_focused_window)
|
||||
.map_or(1, |f| f(xdo, window_ret))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_focused_window_sane(
|
||||
xdo: *const xdo_t,
|
||||
window_ret: *mut Window,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_focused_window_sane)
|
||||
.map_or(1, |f| f(xdo, window_ret))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_window_location(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
x: *mut c_int,
|
||||
y: *mut c_int,
|
||||
screen_ret: *mut *mut Screen,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_window_location)
|
||||
.map_or(1, |f| f(xdo, window, x, y, screen_ret))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_window_size(
|
||||
xdo: *const xdo_t,
|
||||
window: Window,
|
||||
width: *mut c_uint,
|
||||
height: *mut c_uint,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_window_size)
|
||||
.map_or(1, |f| f(xdo, window, width, height))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_get_input_state(xdo: *const xdo_t) -> c_uint {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_get_input_state)
|
||||
.map_or(0, |f| f(xdo))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_activate_window(xdo: *const xdo_t, wid: Window) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_activate_window)
|
||||
.map_or(1, |f| f(xdo, wid))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_wait_for_mouse_move_from(
|
||||
xdo: *const xdo_t,
|
||||
origin_x: c_int,
|
||||
origin_y: c_int,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_wait_for_mouse_move_from)
|
||||
.map_or(1, |f| f(xdo, origin_x, origin_y))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_wait_for_mouse_move_to(
|
||||
xdo: *const xdo_t,
|
||||
dest_x: c_int,
|
||||
dest_y: c_int,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_wait_for_mouse_move_to)
|
||||
.map_or(1, |f| f(xdo, dest_x, dest_y))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_set_window_class(
|
||||
xdo: *const xdo_t,
|
||||
wid: Window,
|
||||
name: *const c_char,
|
||||
class: *const c_char,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_set_window_class)
|
||||
.map_or(1, |f| f(xdo, wid, name, class))
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn xdo_search_windows(
|
||||
xdo: *const xdo_t,
|
||||
search: *const xdo_search_t,
|
||||
windowlist_ret: *mut *mut Window,
|
||||
nwindows_ret: *mut c_uint,
|
||||
) -> c_int {
|
||||
get_lib()
|
||||
.and_then(|lib| lib.xdo_search_windows)
|
||||
.map_or(1, |f| f(xdo, search, windowlist_ret, nwindows_ret))
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.4"
|
||||
version = "1.4.5"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -187,7 +187,10 @@ fn main() {
|
||||
i += 1;
|
||||
}
|
||||
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
|
||||
let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe");
|
||||
#[cfg(windows)]
|
||||
let quick_support = args.is_empty() && win::is_quick_support_exe(&arg_exe);
|
||||
#[cfg(not(windows))]
|
||||
let quick_support = false;
|
||||
|
||||
let mut ui = false;
|
||||
let reader = BinaryReader::default();
|
||||
@@ -234,4 +237,12 @@ mod win {
|
||||
.output();
|
||||
let _allow_err = std::fs::copy(src, &format!("{}\\{}", dir.to_string_lossy(), tgt));
|
||||
}
|
||||
|
||||
/// Check if the executable is a Quick Support version.
|
||||
/// Note: This function must be kept in sync with `src/core_main.rs`.
|
||||
#[inline]
|
||||
pub(super) fn is_quick_support_exe(exe: &str) -> bool {
|
||||
let exe = exe.to_lowercase();
|
||||
exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.4.4
|
||||
pkgver=1.4.5
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
18
res/ab.py
18
res/ab.py
@@ -34,9 +34,10 @@ def view_shared_abs(url, token, name=None):
|
||||
filtered_params["pageSize"] = pageSize
|
||||
|
||||
abs = []
|
||||
current = 1
|
||||
current = 0
|
||||
|
||||
while True:
|
||||
current += 1
|
||||
filtered_params["current"] = current
|
||||
response = requests.get(f"{url}/api/ab/shared/profiles", headers=headers, params=filtered_params)
|
||||
if response.status_code != 200:
|
||||
@@ -52,8 +53,7 @@ def view_shared_abs(url, token, name=None):
|
||||
abs.extend(data)
|
||||
|
||||
total = response_json.get("total", 0)
|
||||
current += pageSize
|
||||
if len(data) < pageSize or current > total:
|
||||
if len(data) < pageSize or current * pageSize >= total:
|
||||
break
|
||||
|
||||
return abs
|
||||
@@ -86,9 +86,10 @@ def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None):
|
||||
filtered_params["pageSize"] = pageSize
|
||||
|
||||
peers = []
|
||||
current = 1
|
||||
current = 0
|
||||
|
||||
while True:
|
||||
current += 1
|
||||
filtered_params["current"] = current
|
||||
response = requests.get(f"{url}/api/ab/peers", headers=headers, params=filtered_params)
|
||||
if response.status_code != 200:
|
||||
@@ -104,8 +105,7 @@ def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None):
|
||||
peers.extend(data)
|
||||
|
||||
total = response_json.get("total", 0)
|
||||
current += pageSize
|
||||
if len(data) < pageSize or current > total:
|
||||
if len(data) < pageSize or current * pageSize >= total:
|
||||
break
|
||||
|
||||
return peers
|
||||
@@ -403,9 +403,10 @@ def view_ab_rules(url, token, ab_guid):
|
||||
}
|
||||
|
||||
rules = []
|
||||
current = 1
|
||||
current = 0
|
||||
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
response = requests.get(f"{url}/api/ab/rules", headers=headers, params=params)
|
||||
if response.status_code != 200:
|
||||
@@ -421,8 +422,7 @@ def view_ab_rules(url, token, ab_guid):
|
||||
rules.extend(data)
|
||||
|
||||
total = response_json.get("total", 0)
|
||||
current += pageSize
|
||||
if len(data) < pageSize or current > total:
|
||||
if len(data) < pageSize or current * pageSize >= total:
|
||||
break
|
||||
|
||||
# Convert numeric permissions to string format
|
||||
|
||||
@@ -42,8 +42,9 @@ def list_groups(url, token, name=None, page_size=50):
|
||||
params = {"pageSize": page_size}
|
||||
if name:
|
||||
params["name"] = name
|
||||
data, current = [], 1
|
||||
data, current = [], 0
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/device-groups", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
@@ -56,8 +57,7 @@ def list_groups(url, token, name=None, page_size=50):
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
if len(rows) < page_size or current * page_size >= total:
|
||||
break
|
||||
return data
|
||||
|
||||
@@ -142,8 +142,9 @@ def view_devices(url, token, group_name=None, id=None, device_name=None,
|
||||
|
||||
params["pageSize"] = page_size
|
||||
|
||||
data, current = [], 1
|
||||
data, current = [], 0
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/devices", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
@@ -152,8 +153,7 @@ def view_devices(url, token, group_name=None, id=None, device_name=None,
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
if len(rows) < page_size or current * page_size >= total:
|
||||
break
|
||||
return data
|
||||
|
||||
@@ -34,9 +34,10 @@ def view(
|
||||
|
||||
devices = []
|
||||
|
||||
current = 1
|
||||
current = 0
|
||||
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
response = requests.get(f"{url}/api/devices", headers=headers, params=params)
|
||||
if response.status_code != 200:
|
||||
@@ -61,8 +62,7 @@ def view(
|
||||
devices.append(device)
|
||||
|
||||
total = response_json.get("total", 0)
|
||||
current += pageSize
|
||||
if len(data) < pageSize or current > total:
|
||||
if len(data) < pageSize or current * pageSize >= total:
|
||||
break
|
||||
|
||||
return devices
|
||||
|
||||
@@ -205,9 +205,13 @@ def sign_files(dir_path, only_ext=None):
|
||||
if not only_ext[i].startswith("."):
|
||||
only_ext[i] = "." + only_ext[i]
|
||||
for root, dirs, files in os.walk(dir_path):
|
||||
is_signed_dir = "RustDeskPrinterDriver" in root or "usbmmidd_v2" in root
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
_, ext = os.path.splitext(file_path)
|
||||
# only sign the exe files in signed dirs
|
||||
if is_signed_dir and ext not in [".exe"]:
|
||||
continue
|
||||
if only_ext and ext not in only_ext:
|
||||
continue
|
||||
if ext in SIGN_EXTENSIONS:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.4
|
||||
Version: 1.4.5
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
URL: https://rustdesk.com
|
||||
Vendor: rustdesk <info@rustdesk.com>
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Recommends: libayatana-appindicator3-1 xdotool
|
||||
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)
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.4
|
||||
Version: 1.4.5
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
URL: https://rustdesk.com
|
||||
Vendor: rustdesk <info@rustdesk.com>
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-base
|
||||
Recommends: libayatana-appindicator-gtk3
|
||||
Requires: gtk3 libxcb libXfixes alsa-lib libva pam gstreamer1-plugins-base
|
||||
Recommends: libayatana-appindicator-gtk3 libxdo
|
||||
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)
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
|
||||
|
||||
@@ -3,8 +3,8 @@ Version: 1.1.9
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Recommends: libayatana-appindicator3-1
|
||||
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
|
||||
Recommends: libayatana-appindicator3-1 xdotool
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.4
|
||||
Version: 1.4.5
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
URL: https://rustdesk.com
|
||||
Vendor: rustdesk <info@rustdesk.com>
|
||||
Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva2 pam gstreamer1-plugins-base
|
||||
Recommends: libayatana-appindicator-gtk3
|
||||
Requires: gtk3 libxcb libXfixes alsa-lib libva2 pam gstreamer1-plugins-base
|
||||
Recommends: libayatana-appindicator-gtk3 libxdo
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
|
||||
|
||||
|
||||
@@ -42,8 +42,9 @@ def list_groups(url, token, name=None, page_size=50):
|
||||
params = {"pageSize": page_size}
|
||||
if name:
|
||||
params["name"] = name
|
||||
data, current = [], 1
|
||||
data, current = [], 0
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/user-groups", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
@@ -56,8 +57,7 @@ def list_groups(url, token, name=None, page_size=50):
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
if len(rows) < page_size or current * page_size >= total:
|
||||
break
|
||||
return data
|
||||
|
||||
@@ -142,8 +142,9 @@ def view_users(url, token, group_name=None, name=None, page_size=50):
|
||||
|
||||
params["pageSize"] = page_size
|
||||
|
||||
data, current = [], 1
|
||||
data, current = [], 0
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
r = requests.get(f"{url}/api/users", headers=headers, params=params)
|
||||
if r.status_code != 200:
|
||||
@@ -152,8 +153,7 @@ def view_users(url, token, group_name=None, name=None, page_size=50):
|
||||
rows = res.get("data", [])
|
||||
data.extend(rows)
|
||||
total = res.get("total", 0)
|
||||
current += page_size
|
||||
if len(rows) < page_size or current > total:
|
||||
if len(rows) < page_size or current * page_size >= total:
|
||||
break
|
||||
return data
|
||||
|
||||
@@ -49,9 +49,10 @@ def view(
|
||||
|
||||
users = []
|
||||
|
||||
current = 1
|
||||
current = 0
|
||||
|
||||
while True:
|
||||
current += 1
|
||||
params["current"] = current
|
||||
response = requests.get(f"{url}/api/users", headers=headers, params=params)
|
||||
if response.status_code != 200:
|
||||
@@ -67,8 +68,7 @@ def view(
|
||||
users.extend(data)
|
||||
|
||||
total = response_json.get("total", 0)
|
||||
current += pageSize
|
||||
if len(data) < pageSize or current > total:
|
||||
if len(data) < pageSize or current * pageSize >= total:
|
||||
break
|
||||
|
||||
return users
|
||||
|
||||
@@ -1054,7 +1054,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
pub async fn sync_jobs_status_to_local(&mut self) -> bool {
|
||||
log::info!("sync transfer job status");
|
||||
if !self.is_connected {
|
||||
return false;
|
||||
}
|
||||
let mut config: PeerConfig = self.handler.load_config();
|
||||
let mut transfer_metas = TransferSerde::default();
|
||||
for job in self.read_jobs.iter() {
|
||||
@@ -1674,6 +1676,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
Some(file_response::Union::Error(e)) => {
|
||||
let job_type = fs::remove_job(e.id, &mut self.write_jobs)
|
||||
.or_else(|| fs::remove_job(e.id, &mut self.read_jobs))
|
||||
.map(|j| j.r#type)
|
||||
.unwrap_or(fs::JobType::Generic);
|
||||
match job_type {
|
||||
|
||||
129
src/common.rs
129
src/common.rs
@@ -71,6 +71,19 @@ pub mod input {
|
||||
pub const MOUSE_TYPE_UP: i32 = 2;
|
||||
pub const MOUSE_TYPE_WHEEL: i32 = 3;
|
||||
pub const MOUSE_TYPE_TRACKPAD: i32 = 4;
|
||||
/// Relative mouse movement type for gaming/3D applications.
|
||||
/// This type sends delta (dx, dy) values instead of absolute coordinates.
|
||||
/// NOTE: This is only supported by the Flutter client. The Sciter client (deprecated)
|
||||
/// does not support relative mouse mode due to:
|
||||
/// 1. Fixed send_mouse() function signature that doesn't allow type differentiation
|
||||
/// 2. Lack of pointer lock API in Sciter/TIS
|
||||
/// 3. No OS cursor control (hide/show/clip) FFI bindings in Sciter UI
|
||||
pub const MOUSE_TYPE_MOVE_RELATIVE: i32 = 5;
|
||||
|
||||
/// Mask to extract the mouse event type from the mask field.
|
||||
/// The lower 3 bits contain the event type (MOUSE_TYPE_*), giving a valid range of 0-7.
|
||||
/// Currently defined types use values 0-5; values 6 and 7 are reserved for future use.
|
||||
pub const MOUSE_TYPE_MASK: i32 = 0x7;
|
||||
|
||||
pub const MOUSE_BUTTON_LEFT: i32 = 0x01;
|
||||
pub const MOUSE_BUTTON_RIGHT: i32 = 0x02;
|
||||
@@ -115,10 +128,6 @@ pub fn global_init() -> bool {
|
||||
crate::server::wayland::init();
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::platform::macos::try_remove_temp_update_dir(None);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -179,12 +188,38 @@ pub fn is_support_file_transfer_resume_num(ver: i64) -> bool {
|
||||
ver >= hbb_common::get_version_number("1.4.2")
|
||||
}
|
||||
|
||||
/// Minimum server version required for relative mouse mode support.
|
||||
/// This constant must mirror Flutter's `kMinVersionForRelativeMouseMode` in `consts.dart`.
|
||||
const MIN_VERSION_RELATIVE_MOUSE_MODE: &str = "1.4.5";
|
||||
|
||||
#[inline]
|
||||
pub fn is_support_relative_mouse_mode(ver: &str) -> bool {
|
||||
is_support_relative_mouse_mode_num(hbb_common::get_version_number(ver))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_support_relative_mouse_mode_num(ver: i64) -> bool {
|
||||
ver >= hbb_common::get_version_number(MIN_VERSION_RELATIVE_MOUSE_MODE)
|
||||
}
|
||||
|
||||
// is server process, with "--server" args
|
||||
#[inline]
|
||||
pub fn is_server() -> bool {
|
||||
*IS_SERVER
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn need_fs_cm_send_files() -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
is_server()
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_main() -> bool {
|
||||
*IS_MAIN
|
||||
@@ -1037,10 +1072,6 @@ fn get_api_server_(api: String, custom: String) -> String {
|
||||
if !api.is_empty() {
|
||||
return api.to_owned();
|
||||
}
|
||||
let api = option_env!("API_SERVER").unwrap_or_default();
|
||||
if !api.is_empty() {
|
||||
return api.into();
|
||||
}
|
||||
let s0 = get_custom_rendezvous_server(custom);
|
||||
if !s0.is_empty() {
|
||||
let s = crate::increase_port(&s0, -2);
|
||||
@@ -1055,7 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String {
|
||||
|
||||
#[inline]
|
||||
pub fn is_public(url: &str) -> bool {
|
||||
url.contains("rustdesk.com")
|
||||
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
||||
}
|
||||
|
||||
pub fn get_udp_punch_enabled() -> bool {
|
||||
@@ -1702,8 +1733,7 @@ pub fn create_symmetric_key_msg(their_pk_b: [u8; 32]) -> (Bytes, Bytes, secretbo
|
||||
|
||||
#[inline]
|
||||
pub fn using_public_server() -> bool {
|
||||
option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty()
|
||||
&& crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty()
|
||||
crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty()
|
||||
}
|
||||
|
||||
pub struct ThrottledInterval {
|
||||
@@ -2269,6 +2299,28 @@ pub fn str2color(s: &str, alpha: u8) -> u32 {
|
||||
(alpha as u32) << 24 | rgb
|
||||
}
|
||||
|
||||
/// Check control permission state from a u64 bitmap.
|
||||
/// Each permission uses 2 bits: 0 = not set, 1 = disable, 2 = enable, 3 = invalid (treated as not set)
|
||||
/// Returns: Some(true) = enabled, Some(false) = disabled, None = not set or invalid
|
||||
pub fn get_control_permission(
|
||||
permissions: u64,
|
||||
permission: hbb_common::rendezvous_proto::control_permissions::Permission,
|
||||
) -> Option<bool> {
|
||||
use hbb_common::protobuf::Enum;
|
||||
let index = permission.value();
|
||||
if index >= 0 && index < 32 {
|
||||
let shift = index * 2;
|
||||
let value = (permissions >> shift) & 0b11;
|
||||
match value {
|
||||
1 => Some(false), // disable
|
||||
2 => Some(true), // enable
|
||||
_ => None, // 0 = not set, 3 = invalid
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2409,4 +2461,59 @@ mod tests {
|
||||
Duration::from_nanos(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_public() {
|
||||
// Test URLs containing "rustdesk.com/"
|
||||
assert!(is_public("https://rustdesk.com/"));
|
||||
assert!(is_public("https://www.rustdesk.com/"));
|
||||
assert!(is_public("https://api.rustdesk.com/v1"));
|
||||
assert!(is_public("https://rustdesk.com/path"));
|
||||
|
||||
// Test URLs ending with "rustdesk.com"
|
||||
assert!(is_public("rustdesk.com"));
|
||||
assert!(is_public("https://rustdesk.com"));
|
||||
assert!(is_public("http://www.rustdesk.com"));
|
||||
assert!(is_public("https://api.rustdesk.com"));
|
||||
|
||||
// Test non-public URLs
|
||||
assert!(!is_public("https://example.com"));
|
||||
assert!(!is_public("https://custom-server.com"));
|
||||
assert!(!is_public("http://192.168.1.1"));
|
||||
assert!(!is_public("localhost"));
|
||||
assert!(!is_public("https://rustdesk.computer.com"));
|
||||
assert!(!is_public("rustdesk.comhello.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mouse_event_constants_and_mask_layout() {
|
||||
use super::input::*;
|
||||
|
||||
// Verify MOUSE_TYPE constants are unique and within the mask range.
|
||||
let types = [
|
||||
MOUSE_TYPE_MOVE,
|
||||
MOUSE_TYPE_DOWN,
|
||||
MOUSE_TYPE_UP,
|
||||
MOUSE_TYPE_WHEEL,
|
||||
MOUSE_TYPE_TRACKPAD,
|
||||
MOUSE_TYPE_MOVE_RELATIVE,
|
||||
];
|
||||
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for t in types.iter() {
|
||||
assert!(seen.insert(*t), "Duplicate mouse type: {}", t);
|
||||
assert_eq!(
|
||||
*t & MOUSE_TYPE_MASK,
|
||||
*t,
|
||||
"Mouse type {} exceeds mask {}",
|
||||
t,
|
||||
MOUSE_TYPE_MASK
|
||||
);
|
||||
}
|
||||
|
||||
// The mask layout is: lower 3 bits for type, upper bits for buttons (shifted by 3).
|
||||
let combined_mask = MOUSE_TYPE_DOWN | ((MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT) << 3);
|
||||
assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN);
|
||||
assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
{
|
||||
_is_quick_support |= !crate::platform::is_installed()
|
||||
&& args.is_empty()
|
||||
&& (arg_exe.to_lowercase().contains("-qs-")
|
||||
&& (is_quick_support_exe(&arg_exe)
|
||||
|| config::LocalConfig::get_option("pre-elevate-service") == "Y"
|
||||
|| (!click_setup && crate::platform::is_elevated(None).unwrap_or(false)));
|
||||
crate::portable_service::client::set_quick_support(_is_quick_support);
|
||||
@@ -181,6 +181,11 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
init_plugins(&args);
|
||||
if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::platform::macos::try_remove_temp_update_dir(None);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
hbb_common::config::PeerConfig::preload_peers();
|
||||
std::thread::spawn(move || crate::start_server(false, no_server));
|
||||
@@ -401,6 +406,10 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
println!("Settings are disabled!");
|
||||
return None;
|
||||
}
|
||||
if config::Config::is_disable_change_permanent_password() {
|
||||
println!("Changing permanent password is disabled!");
|
||||
return None;
|
||||
}
|
||||
if args.len() == 2 {
|
||||
if crate::platform::is_installed() && is_root() {
|
||||
if let Err(err) = crate::ipc::set_permanent_password(args[1].to_owned()) {
|
||||
@@ -414,6 +423,10 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
}
|
||||
return None;
|
||||
} else if args[0] == "--set-unlock-pin" {
|
||||
if config::Config::is_disable_unlock_pin() {
|
||||
println!("Unlock PIN is disabled!");
|
||||
return None;
|
||||
}
|
||||
#[cfg(feature = "flutter")]
|
||||
if args.len() == 2 {
|
||||
if crate::platform::is_installed() && is_root() {
|
||||
@@ -435,6 +448,10 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
println!("Settings are disabled!");
|
||||
return None;
|
||||
}
|
||||
if config::Config::is_disable_change_id() {
|
||||
println!("Changing ID is disabled!");
|
||||
return None;
|
||||
}
|
||||
if args.len() == 2 {
|
||||
if crate::platform::is_installed() && is_root() {
|
||||
let old_id = crate::ipc::get_id();
|
||||
@@ -598,6 +615,17 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
#[cfg(feature = "hwcodec")]
|
||||
crate::ipc::hwcodec_process();
|
||||
return None;
|
||||
} else if args[0] == "--terminal-helper" {
|
||||
// Terminal helper process - runs as user to create ConPTY
|
||||
// This is needed because ConPTY has compatibility issues with CreateProcessAsUserW
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let helper_args: Vec<String> = args[1..].to_vec();
|
||||
if let Err(e) = crate::server::terminal_helper::run_terminal_helper(&helper_args) {
|
||||
log::error!("Terminal helper failed: {}", e);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
} else if args[0] == "--cm" {
|
||||
// call connection manager to establish connections
|
||||
// meanwhile, return true to call flutter window to show control panel
|
||||
@@ -801,3 +829,12 @@ fn is_root() -> bool {
|
||||
#[allow(unreachable_code)]
|
||||
crate::platform::is_root()
|
||||
}
|
||||
|
||||
/// Check if the executable is a Quick Support version.
|
||||
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
|
||||
#[cfg(windows)]
|
||||
#[inline]
|
||||
fn is_quick_support_exe(exe: &str) -> bool {
|
||||
let exe = exe.to_lowercase();
|
||||
exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe")
|
||||
}
|
||||
|
||||
@@ -1215,6 +1215,66 @@ pub fn main_set_input_source(session_id: SessionID, value: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set cursor position (for pointer lock re-centering).
|
||||
///
|
||||
/// # Returns
|
||||
/// - `true`: cursor position was successfully set
|
||||
/// - `false`: operation failed or not supported
|
||||
///
|
||||
/// # Platform behavior
|
||||
/// - Windows/macOS/Linux: attempts to move the cursor to (x, y)
|
||||
/// - Android/iOS: no-op, always returns `false`
|
||||
pub fn main_set_cursor_position(x: i32, y: i32) -> SyncReturn<bool> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
SyncReturn(crate::set_cursor_pos(x, y))
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let _ = (x, y);
|
||||
SyncReturn(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip cursor to a rectangle (for pointer lock).
|
||||
///
|
||||
/// When `enable` is true, the cursor is clipped to the rectangle defined by
|
||||
/// `left`, `top`, `right`, `bottom`. When `enable` is false, the rectangle
|
||||
/// values are ignored and the cursor is unclipped.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `true`: operation succeeded or no-op completed
|
||||
/// - `false`: operation failed
|
||||
///
|
||||
/// # Platform behavior
|
||||
/// - Windows: uses ClipCursor API to confine cursor to the specified rectangle
|
||||
/// - macOS: uses CGAssociateMouseAndMouseCursorPosition for pointer lock effect;
|
||||
/// the rect coordinates are ignored (only Some/None matters)
|
||||
/// - Linux: no-op, always returns `true`; use pointer warping for similar effect
|
||||
/// - Android/iOS: no-op, always returns `false`
|
||||
pub fn main_clip_cursor(
|
||||
left: i32,
|
||||
top: i32,
|
||||
right: i32,
|
||||
bottom: i32,
|
||||
enable: bool,
|
||||
) -> SyncReturn<bool> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
let rect = if enable {
|
||||
Some((left, top, right, bottom))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
SyncReturn(crate::clip_cursor(rect))
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let _ = (left, top, right, bottom, enable);
|
||||
SyncReturn(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_get_my_id() -> String {
|
||||
get_id()
|
||||
}
|
||||
@@ -1748,8 +1808,99 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) {
|
||||
super::flutter::session_send_pointer(session_id, msg);
|
||||
}
|
||||
|
||||
/// Send mouse event from Flutter to the remote peer.
|
||||
///
|
||||
/// # Relative Mouse Mode Message Contract
|
||||
///
|
||||
/// When the message contains a `relative_mouse_mode` field, this function validates
|
||||
/// and filters activation/deactivation markers.
|
||||
///
|
||||
/// **Mode Authority:**
|
||||
/// The Flutter InputModel is authoritative for relative mouse mode activation/deactivation.
|
||||
/// The server (via `input_service.rs`) only consumes forwarded delta movements and tracks
|
||||
/// relative movement processing state, but does NOT control mode activation/deactivation.
|
||||
///
|
||||
/// **Deactivation Markers are Local-Only:**
|
||||
/// Deactivation markers (`relative_mouse_mode: "0"`) are NEVER forwarded to the server.
|
||||
/// They are handled entirely on the client side to reset local UI state (cursor visibility,
|
||||
/// pointer lock, etc.). The server does not rely on deactivation markers and should not
|
||||
/// expect to receive them.
|
||||
///
|
||||
/// **Contract (Flutter side MUST adhere to):**
|
||||
/// 1. `relative_mouse_mode` field is ONLY present on activation/deactivation marker messages,
|
||||
/// NEVER on normal pointer events (move, button, scroll).
|
||||
/// 2. Deactivation marker: `{"relative_mouse_mode": "0"}` - local-only, never forwarded.
|
||||
/// 3. Activation marker: `{"relative_mouse_mode": "1", "type": "move_relative", "x": "0", "y": "0"}`
|
||||
/// - MUST use `type="move_relative"` with `x="0"` and `y="0"` (safe no-op).
|
||||
/// - Any other combination is dropped to prevent accidental cursor movement.
|
||||
///
|
||||
/// If these assumptions are violated (e.g., `relative_mouse_mode` is added to normal events),
|
||||
/// legitimate mouse events may be silently dropped by the early-return logic below.
|
||||
pub fn session_send_mouse(session_id: SessionID, msg: String) {
|
||||
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
|
||||
// Relative mouse mode marker validation (Flutter-only).
|
||||
// This only validates and filters markers; the server tracks per-connection
|
||||
// relative-movement processing state but not mode activation/deactivation.
|
||||
// See doc comment above for the message contract.
|
||||
if let Some(v) = m.get("relative_mouse_mode") {
|
||||
let active = matches!(v.as_str(), "1" | "Y" | "on");
|
||||
|
||||
// Disable marker: local-only, never forwarded to the server.
|
||||
// The server does not track mode deactivation; it simply stops receiving
|
||||
// relative move events when the client exits relative mouse mode.
|
||||
if !active {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::keyboard::set_relative_mouse_mode_state(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable marker: validate BEFORE setting state to avoid desync.
|
||||
// This ensures we only mark as active if the marker will actually be forwarded.
|
||||
|
||||
// Enable marker is allowed to go through only if it's a safe no-op relative move.
|
||||
// This avoids accidentally moving the remote cursor (e.g. if type/x/y are missing).
|
||||
let msg_type = m.get("type").map(|t| t.as_str());
|
||||
if msg_type != Some("move_relative") {
|
||||
log::warn!(
|
||||
"relative_mouse_mode activation marker has invalid type: {:?}, expected 'move_relative'. Dropping.",
|
||||
msg_type
|
||||
);
|
||||
return;
|
||||
}
|
||||
let x_marker = m
|
||||
.get("x")
|
||||
.map(|x| x.parse::<i32>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
let y_marker = m
|
||||
.get("y")
|
||||
.map(|y| y.parse::<i32>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
if x_marker != 0 || y_marker != 0 {
|
||||
log::warn!(
|
||||
"relative_mouse_mode activation marker has non-zero coordinates: x={}, y={}. Dropping.",
|
||||
x_marker, y_marker
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against unexpected fields that could turn this no-op into a real event.
|
||||
if m.contains_key("buttons")
|
||||
|| m.contains_key("alt")
|
||||
|| m.contains_key("ctrl")
|
||||
|| m.contains_key("shift")
|
||||
|| m.contains_key("command")
|
||||
{
|
||||
log::warn!(
|
||||
"relative_mouse_mode activation marker contains unexpected fields (buttons/alt/ctrl/shift/command). Dropping."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// All validation passed - marker will be forwarded as a no-op relative move.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::keyboard::set_relative_mouse_mode_state(true);
|
||||
}
|
||||
|
||||
let alt = m.get("alt").is_some();
|
||||
let ctrl = m.get("ctrl").is_some();
|
||||
let shift = m.get("shift").is_some();
|
||||
@@ -1769,6 +1920,7 @@ pub fn session_send_mouse(session_id: SessionID, msg: String) {
|
||||
"up" => MOUSE_TYPE_UP,
|
||||
"wheel" => MOUSE_TYPE_WHEEL,
|
||||
"trackpad" => MOUSE_TYPE_TRACKPAD,
|
||||
"move_relative" => MOUSE_TYPE_MOVE_RELATIVE,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
@@ -2600,6 +2752,13 @@ pub fn main_get_common(key: String) -> String {
|
||||
return false.to_string();
|
||||
} else if key == "transfer-job-id" {
|
||||
return hbb_common::fs::get_next_job_id().to_string();
|
||||
} else if key == "is-remote-modify-enabled-by-control-permissions" {
|
||||
return match is_remote_modify_enabled_by_control_permissions() {
|
||||
Some(true) => "true",
|
||||
Some(false) => "false",
|
||||
None => "",
|
||||
}
|
||||
.to_string();
|
||||
} else {
|
||||
if key.starts_with("download-data-") {
|
||||
let id = key.replace("download-data-", "");
|
||||
|
||||
@@ -278,7 +278,7 @@ fn heartbeat_url() -> String {
|
||||
Config::get_option("api-server"),
|
||||
Config::get_option("custom-rendezvous-server"),
|
||||
);
|
||||
if url.is_empty() || url.contains("rustdesk.com") {
|
||||
if url.is_empty() || crate::is_public(&url) {
|
||||
return "".to_owned();
|
||||
}
|
||||
format!("{}/api/heartbeat", url)
|
||||
|
||||
127
src/ipc.rs
127
src/ipc.rs
@@ -23,7 +23,11 @@ pub use clipboard::ClipboardFile;
|
||||
use hbb_common::{
|
||||
allow_err, bail, bytes,
|
||||
bytes_codec::BytesCodec,
|
||||
config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2},
|
||||
config::{
|
||||
self,
|
||||
keys::{self, OPTION_ALLOW_WEBSOCKET},
|
||||
Config, Config2,
|
||||
},
|
||||
futures::StreamExt as _,
|
||||
futures_util::sink::SinkExt,
|
||||
log, password_security as password, timeout,
|
||||
@@ -112,6 +116,33 @@ pub enum FS {
|
||||
path: String,
|
||||
new_name: String,
|
||||
},
|
||||
// CM-side file reading operations (Windows only)
|
||||
// These enable Connection Manager to read files and stream them back to Connection
|
||||
ReadFile {
|
||||
path: String,
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
include_hidden: bool,
|
||||
conn_id: i32,
|
||||
overwrite_detection: bool,
|
||||
},
|
||||
CancelRead {
|
||||
id: i32,
|
||||
conn_id: i32,
|
||||
},
|
||||
SendConfirmForRead {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
skip: bool,
|
||||
offset_blk: u32,
|
||||
conn_id: i32,
|
||||
},
|
||||
ReadAllFiles {
|
||||
path: String,
|
||||
id: i32,
|
||||
include_hidden: bool,
|
||||
conn_id: i32,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -268,6 +299,72 @@ pub enum Data {
|
||||
#[cfg(windows)]
|
||||
ControlledSessionCount(usize),
|
||||
CmErr(String),
|
||||
// CM-side file reading responses (Windows only)
|
||||
// These are sent from CM back to Connection when CM handles file reading
|
||||
/// Response to ReadFile: contains initial file list or error
|
||||
ReadJobInitResult {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
include_hidden: bool,
|
||||
conn_id: i32,
|
||||
/// Serialized protobuf bytes of FileDirectory, or error string
|
||||
result: Result<Vec<u8>, String>,
|
||||
},
|
||||
/// File data block read by CM.
|
||||
///
|
||||
/// The actual data is sent separately via `send_raw()` after this message to avoid
|
||||
/// JSON encoding overhead for large binary data. This mirrors the `WriteBlock` pattern.
|
||||
///
|
||||
/// **Protocol:**
|
||||
/// - Sender: `send(FileBlockFromCM{...})` then `send_raw(data)`
|
||||
/// - Receiver: `next()` returns `FileBlockFromCM`, then `next_raw()` returns data bytes
|
||||
///
|
||||
/// **Note on empty data (e.g., empty files):**
|
||||
/// Empty data is supported. The IPC connection uses `BytesCodec` with `raw=false` (default),
|
||||
/// which prefixes each frame with a length header. So `send_raw(Bytes::new())` sends a
|
||||
/// 1-byte frame (length=0), and `next_raw()` correctly returns an empty `BytesMut`.
|
||||
/// See `libs/hbb_common/src/bytes_codec.rs` test `test_codec2` for verification.
|
||||
FileBlockFromCM {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
/// Data is sent separately via `send_raw()` to avoid JSON encoding overhead.
|
||||
/// This field is skipped during serialization; sender must call `send_raw()` after sending.
|
||||
/// Receiver must call `next_raw()` and populate this field manually.
|
||||
#[serde(skip)]
|
||||
data: bytes::Bytes,
|
||||
compressed: bool,
|
||||
conn_id: i32,
|
||||
},
|
||||
/// File read completed successfully
|
||||
FileReadDone {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
conn_id: i32,
|
||||
},
|
||||
/// File read failed with error
|
||||
FileReadError {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
err: String,
|
||||
conn_id: i32,
|
||||
},
|
||||
/// Digest info from CM for overwrite detection
|
||||
FileDigestFromCM {
|
||||
id: i32,
|
||||
file_num: i32,
|
||||
last_modified: u64,
|
||||
file_size: u64,
|
||||
is_resume: bool,
|
||||
conn_id: i32,
|
||||
},
|
||||
/// Response to ReadAllFiles: recursive directory listing
|
||||
AllFilesResult {
|
||||
id: i32,
|
||||
conn_id: i32,
|
||||
path: String,
|
||||
/// Serialized protobuf bytes of FileDirectory, or error string
|
||||
result: Result<Vec<u8>, String>,
|
||||
},
|
||||
CheckHwcodec,
|
||||
#[cfg(feature = "flutter")]
|
||||
VideoConnCount(Option<usize>),
|
||||
@@ -291,6 +388,9 @@ pub enum Data {
|
||||
SocksWs(Option<Box<(Option<config::Socks5Server>, String)>>),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Whiteboard((String, crate::whiteboard::CustomEvent)),
|
||||
ControlPermissionsRemoteModify(Option<bool>),
|
||||
#[cfg(target_os = "windows")]
|
||||
FileTransferEnabledState(Option<bool>),
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
@@ -769,6 +869,31 @@ async fn handle(data: Data, stream: &mut Connection) {
|
||||
// Port forward session count is only a get value.
|
||||
}
|
||||
},
|
||||
Data::ControlPermissionsRemoteModify(_) => {
|
||||
use hbb_common::rendezvous_proto::control_permissions::Permission;
|
||||
let state =
|
||||
crate::server::get_control_permission_state(Permission::remote_modify, true);
|
||||
allow_err!(
|
||||
stream
|
||||
.send(&Data::ControlPermissionsRemoteModify(state))
|
||||
.await
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
Data::FileTransferEnabledState(_) => {
|
||||
use hbb_common::rendezvous_proto::control_permissions::Permission;
|
||||
let state = crate::server::get_control_permission_state(Permission::file, false);
|
||||
let enabled = state.unwrap_or_else(|| {
|
||||
crate::server::Connection::is_permission_enabled_locally(
|
||||
config::keys::OPTION_ENABLE_FILE_TRANSFER,
|
||||
)
|
||||
});
|
||||
allow_err!(
|
||||
stream
|
||||
.send(&Data::FileTransferEnabledState(Some(enabled)))
|
||||
.await
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
171
src/keyboard.rs
171
src/keyboard.rs
@@ -32,9 +32,33 @@ const OS_LOWER_MACOS: &str = "macos";
|
||||
#[allow(dead_code)]
|
||||
const OS_LOWER_ANDROID: &str = "android";
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// Track key down state for relative mouse mode exit shortcut.
|
||||
// macOS: Cmd+G (track G key)
|
||||
// Windows/Linux: Ctrl+Alt (track whichever modifier was pressed last)
|
||||
// This prevents the exit from retriggering on OS key-repeat.
|
||||
#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
static EXIT_SHORTCUT_KEY_DOWN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// Track whether relative mouse mode is currently active.
|
||||
// This is set by Flutter via set_relative_mouse_mode_state() and checked
|
||||
// by the rdev grab loop to determine if exit shortcuts should be processed.
|
||||
#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
static RELATIVE_MOUSE_MODE_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Set the relative mouse mode state from Flutter.
|
||||
/// This is called when entering or exiting relative mouse mode.
|
||||
#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
pub fn set_relative_mouse_mode_state(active: bool) {
|
||||
RELATIVE_MOUSE_MODE_ACTIVE.store(active, Ordering::SeqCst);
|
||||
// Reset exit shortcut state when mode changes to avoid stale state
|
||||
if !active {
|
||||
EXIT_SHORTCUT_KEY_DOWN.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
@@ -82,7 +106,7 @@ pub mod client {
|
||||
GrabState::Run => {
|
||||
#[cfg(windows)]
|
||||
update_grab_get_key_name(keyboard_mode);
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
KEYBOARD_HOOKED.swap(true, Ordering::SeqCst);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -94,7 +118,7 @@ pub mod client {
|
||||
|
||||
release_remote_keys(keyboard_mode);
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
KEYBOARD_HOOKED.swap(false, Ordering::SeqCst);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -266,6 +290,136 @@ fn get_keyboard_mode() -> String {
|
||||
"legacy".to_string()
|
||||
}
|
||||
|
||||
/// Check if exit shortcut for relative mouse mode is active.
|
||||
/// Exit shortcuts (only exits, not toggles):
|
||||
/// - macOS: Cmd+G
|
||||
/// - Windows/Linux: Ctrl+Alt (triggered when both are pressed)
|
||||
/// Note: This shortcut is only available in Flutter client. Sciter client does not support relative mouse mode.
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
fn is_exit_relative_mouse_shortcut(key: Key) -> bool {
|
||||
let modifiers = MODIFIERS_STATE.lock().unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// macOS: Cmd+G to exit
|
||||
if key != Key::KeyG {
|
||||
return false;
|
||||
}
|
||||
let meta = *modifiers.get(&Key::MetaLeft).unwrap_or(&false)
|
||||
|| *modifiers.get(&Key::MetaRight).unwrap_or(&false);
|
||||
return meta;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Windows/Linux: Ctrl+Alt to exit
|
||||
// Triggered when Ctrl is pressed while Alt is down, or Alt is pressed while Ctrl is down
|
||||
let is_ctrl_key = key == Key::ControlLeft || key == Key::ControlRight;
|
||||
let is_alt_key = key == Key::Alt || key == Key::AltGr;
|
||||
|
||||
if !is_ctrl_key && !is_alt_key {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ctrl = *modifiers.get(&Key::ControlLeft).unwrap_or(&false)
|
||||
|| *modifiers.get(&Key::ControlRight).unwrap_or(&false);
|
||||
let alt = *modifiers.get(&Key::Alt).unwrap_or(&false)
|
||||
|| *modifiers.get(&Key::AltGr).unwrap_or(&false);
|
||||
|
||||
// When Ctrl is pressed and Alt is already down, or vice versa
|
||||
(is_ctrl_key && alt) || (is_alt_key && ctrl)
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify Flutter to exit relative mouse mode.
|
||||
/// Note: This is Flutter-only. Sciter client does not support relative mouse mode.
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
fn notify_exit_relative_mouse_mode() {
|
||||
let session_id = flutter::get_cur_session_id();
|
||||
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
|
||||
}
|
||||
|
||||
|
||||
/// Handle relative mouse mode shortcuts in the rdev grab loop.
|
||||
/// Returns true if the event should be blocked from being sent to the peer.
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
#[inline]
|
||||
fn can_exit_relative_mouse_mode_from_grab_loop() -> bool {
|
||||
// Only process exit shortcuts when relative mouse mode is actually active.
|
||||
// This prevents blocking Ctrl+Alt (or Cmd+G) when not in relative mouse mode.
|
||||
if !RELATIVE_MOUSE_MODE_ACTIVE.load(Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(session) = flutter::get_cur_session() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Only for remote desktop sessions.
|
||||
if !session.is_default() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have keyboard permission and not be in view-only mode.
|
||||
if !*session.server_keyboard_enabled.read().unwrap() {
|
||||
return false;
|
||||
}
|
||||
let lc = session.lc.read().unwrap();
|
||||
if lc.view_only.v {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Peer must support relative mouse mode.
|
||||
crate::common::is_support_relative_mouse_mode_num(lc.version)
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
#[inline]
|
||||
fn should_block_relative_mouse_shortcut(key: Key, is_press: bool) -> bool {
|
||||
if !KEYBOARD_HOOKED.load(Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine which key to track for key-up blocking based on platform
|
||||
#[cfg(target_os = "macos")]
|
||||
let is_tracked_key = key == Key::KeyG;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let is_tracked_key = key == Key::ControlLeft
|
||||
|| key == Key::ControlRight
|
||||
|| key == Key::Alt
|
||||
|| key == Key::AltGr;
|
||||
|
||||
// Block key up if key down was blocked (to avoid orphan key up event on remote).
|
||||
// This must be checked before clearing the flag below.
|
||||
if is_tracked_key && !is_press && EXIT_SHORTCUT_KEY_DOWN.swap(false, Ordering::SeqCst) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exit relative mouse mode shortcuts:
|
||||
// - macOS: Cmd+G
|
||||
// - Windows/Linux: Ctrl+Alt
|
||||
// Guard it to supported/eligible sessions to avoid blocking the chord unexpectedly.
|
||||
if is_exit_relative_mouse_shortcut(key) {
|
||||
if !can_exit_relative_mouse_mode_from_grab_loop() {
|
||||
return false;
|
||||
}
|
||||
if is_press {
|
||||
// Only trigger exit on transition from "not pressed" to "pressed".
|
||||
// This prevents retriggering on OS key-repeat.
|
||||
if !EXIT_SHORTCUT_KEY_DOWN.swap(true, Ordering::SeqCst) {
|
||||
notify_exit_relative_mouse_mode();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn start_grab_loop() {
|
||||
std::env::set_var("KEYBOARD_ONLY", "y");
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
@@ -278,6 +432,12 @@ fn start_grab_loop() {
|
||||
|
||||
let _scan_code = event.position_code;
|
||||
let _code = event.platform_code as KeyCode;
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
if should_block_relative_mouse_shortcut(key, is_press) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) {
|
||||
client::process_event(&get_keyboard_mode(), &event, None);
|
||||
if is_press {
|
||||
@@ -337,9 +497,14 @@ fn start_grab_loop() {
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type {
|
||||
EventType::KeyPress(key) | EventType::KeyRelease(key) => {
|
||||
let is_press = matches!(event.event_type, EventType::KeyPress(_));
|
||||
if let Key::Unknown(keycode) = key {
|
||||
log::error!("rdev get unknown key, keycode is {:?}", keycode);
|
||||
} else {
|
||||
#[cfg(feature = "flutter")]
|
||||
if should_block_relative_mouse_shortcut(key, is_press) {
|
||||
return None;
|
||||
}
|
||||
client::process_event(&get_keyboard_mode(), &event, None);
|
||||
}
|
||||
None
|
||||
|
||||
21
src/lang.rs
21
src/lang.rs
@@ -186,7 +186,26 @@ pub fn translate_locale(name: String, locale: &str) -> String {
|
||||
&& !name.starts_with("upgrade_rustdesk_server_pro")
|
||||
&& name != "powered_by_me"
|
||||
{
|
||||
s = s.replace("RustDesk", &crate::get_app_name());
|
||||
let app_name = crate::get_app_name();
|
||||
if !app_name.contains("RustDesk") {
|
||||
s = s.replace("RustDesk", &app_name);
|
||||
} else {
|
||||
// https://github.com/rustdesk/rustdesk-server-pro/issues/845
|
||||
// If app_name contains "RustDesk" (e.g., "RustDesk-Admin"), we need to avoid
|
||||
// replacing "RustDesk" within the already-substituted app_name, which would
|
||||
// cause duplication like "RustDesk-Admin" -> "RustDesk-Admin-Admin".
|
||||
//
|
||||
// app_name only contains alphanumeric and hyphen.
|
||||
const PLACEHOLDER: &str = "#A-P-P-N-A-M-E#";
|
||||
if !s.contains(PLACEHOLDER) {
|
||||
s = s.replace(&app_name, PLACEHOLDER);
|
||||
s = s.replace("RustDesk", &app_name);
|
||||
s = s.replace(PLACEHOLDER, &app_name);
|
||||
} else {
|
||||
// It's very unlikely to reach here.
|
||||
// Skip replacement to avoid incorrect result.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "عرض RustDesk"),
|
||||
("This PC", "هذا الحاسب"),
|
||||
("or", "او"),
|
||||
("Continue with", "متابعة مع"),
|
||||
("Elevate", "ارتقاء"),
|
||||
("Zoom cursor", "تكبير المؤشر"),
|
||||
("Accept sessions via password", "قبول الجلسات عبر كلمة المرور"),
|
||||
@@ -719,15 +718,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Small", "صغير"),
|
||||
("Large", "كبير"),
|
||||
("Show virtual joystick", "إظهار عصا التحكم الافتراضية"),
|
||||
("Edit note", ""),
|
||||
("Alias", ""),
|
||||
("ScrollEdge", ""),
|
||||
("Allow insecure TLS fallback", ""),
|
||||
("allow-insecure-tls-fallback-tip", ""),
|
||||
("Disable UDP", ""),
|
||||
("disable-udp-tip", ""),
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Edit note", "تعديل الملاحظة"),
|
||||
("Alias", "اسم مستعار"),
|
||||
("ScrollEdge", "حافة التمرير"),
|
||||
("Allow insecure TLS fallback", "السماح بالرجوع إلى TLS غير الآمن"),
|
||||
("allow-insecure-tls-fallback-tip", "يسمح باستخدام اتصال TLS غير آمن عند فشل الاتصال الآمن"),
|
||||
("Disable UDP", "تعطيل UDP"),
|
||||
("disable-udp-tip", "عند التفعيل لن يتم استخدام بروتوكول UDP"),
|
||||
("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"),
|
||||
("input note here", "أدخل الملاحظة هنا"),
|
||||
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "متابعة مع {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Паказаць RustDesk"),
|
||||
("This PC", "Гэты кампутар"),
|
||||
("or", "або"),
|
||||
("Continue with", "Працягнуць з"),
|
||||
("Elevate", "Павысіць"),
|
||||
("Zoom cursor", "Павялічэнне курсора"),
|
||||
("Accept sessions via password", "Прымаць сеансы па паролю"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Працягнуць з {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Покажи RustDesk"),
|
||||
("This PC", "Този компютър"),
|
||||
("or", "или"),
|
||||
("Continue with", "Продължи с"),
|
||||
("Elevate", "Повишаване"),
|
||||
("Zoom cursor", "Уголемяване курсор"),
|
||||
("Accept sessions via password", "Приемане сесии чрез парола"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Продължи с {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Mostra el RustDesk"),
|
||||
("This PC", "Aquest equip"),
|
||||
("or", "o"),
|
||||
("Continue with", "Continua amb"),
|
||||
("Elevate", "Permisos ampliats"),
|
||||
("Zoom cursor", "Escala del ratolí"),
|
||||
("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Continua amb {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "显示 RustDesk"),
|
||||
("This PC", "此电脑"),
|
||||
("or", "或"),
|
||||
("Continue with", "使用"),
|
||||
("Elevate", "提权"),
|
||||
("Zoom cursor", "缩放光标"),
|
||||
("Accept sessions via password", "只允许密码访问"),
|
||||
@@ -725,9 +724,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Allow insecure TLS fallback", "允许回退到不安全的 TLS 连接"),
|
||||
("allow-insecure-tls-fallback-tip", "默认情况下,对于使用 TLS 的协议,RustDesk 会验证服务器证书。\n启用此选项后,在验证失败时,RustDesk 将转为跳过验证步骤并继续连接。"),
|
||||
("Disable UDP", "禁用 UDP"),
|
||||
("disable-udp-tip", "控制是否仅使用TCP。\n启用此选项后,RustDesk 将不再使用UDP 21116,而是使用TCP 21116。"),
|
||||
("server-oss-not-support-tip", "注意:RustDesk 开源服务器(OSS server) 不包含此功能。"),
|
||||
("disable-udp-tip", "控制是否仅使用 TCP。\n启用此选项后,RustDesk 将不再使用 UDP 21116,而是使用 TCP 21116。"),
|
||||
("server-oss-not-support-tip", "注意:RustDesk 开源服务器 (OSS server) 不包含此功能。"),
|
||||
("input note here", "输入备注"),
|
||||
("note-at-conn-end-tip", "在连接结束时请求备注"),
|
||||
("Show terminal extra keys", "显示终端扩展键"),
|
||||
("Relative mouse mode", "相对鼠标模式"),
|
||||
("rel-mouse-not-supported-peer-tip", "被控端不支持相对鼠标模式"),
|
||||
("rel-mouse-not-ready-tip", "相对鼠标模式尚未准备好,请稍后再试"),
|
||||
("rel-mouse-lock-failed-tip", "无法锁定鼠标,相对鼠标模式已禁用"),
|
||||
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
|
||||
("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"),
|
||||
("Changelog", "更新日志"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "使用 {} 登录"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Zobrazit RustDesk"),
|
||||
("This PC", "Tento počítač"),
|
||||
("or", "nebo"),
|
||||
("Continue with", "Pokračovat s"),
|
||||
("Elevate", "Zvýšit"),
|
||||
("Zoom cursor", "Kurzor přiblížení"),
|
||||
("Accept sessions via password", "Přijímat relace pomocí hesla"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Pokračovat s {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Vis RustDesk"),
|
||||
("This PC", "Denne PC"),
|
||||
("or", "eller"),
|
||||
("Continue with", "Fortsæt med"),
|
||||
("Elevate", "Elevér"),
|
||||
("Zoom cursor", "Zoom markør"),
|
||||
("Accept sessions via password", "Acceptér sessioner via adgangskode"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Fortsæt med {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "RustDesk anzeigen"),
|
||||
("This PC", "Dieser PC"),
|
||||
("or", "oder"),
|
||||
("Continue with", "Fortfahren mit"),
|
||||
("Elevate", "Zugriff gewähren"),
|
||||
("Zoom cursor", "Cursor vergrößern"),
|
||||
("Accept sessions via password", "Sitzung mit Passwort bestätigen"),
|
||||
@@ -562,8 +561,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "Modus 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktivieren"),
|
||||
("Exit privacy mode", "Datenschutzmodus beenden"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktiviert"),
|
||||
("Exit privacy mode", "Datenschutzmodus beendet"),
|
||||
("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."),
|
||||
("input_source_1_tip", "Eingangsquelle 1"),
|
||||
("input_source_2_tip", "Eingangsquelle 2"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", "HINWEIS: RustDesk Server OSS enthält diese Funktion nicht."),
|
||||
("input note here", "Hier eine Notiz eingeben"),
|
||||
("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."),
|
||||
("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"),
|
||||
("Relative mouse mode", "Relativer Mausmodus"),
|
||||
("rel-mouse-not-supported-peer-tip", "Der relative Mausmodus wird von der verbundenen Gegenstelle nicht unterstützt."),
|
||||
("rel-mouse-not-ready-tip", "Der relative Mausmodus ist noch nicht bereit. Bitte versuchen Sie es erneut."),
|
||||
("rel-mouse-lock-failed-tip", "Cursor konnte nicht gesperrt werden. Der relative Mausmodus wurde deaktiviert."),
|
||||
("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."),
|
||||
("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."),
|
||||
("Changelog", "Änderungsprotokoll"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
|
||||
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
|
||||
("Continue with {}", "Fortfahren mit {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Εμφάνιση RustDesk"),
|
||||
("This PC", "Αυτός ο υπολογιστής"),
|
||||
("or", "ή"),
|
||||
("Continue with", "Συνέχεια με"),
|
||||
("Elevate", "Ανύψωση"),
|
||||
("Zoom cursor", "Kέρσορας μεγέθυνσης"),
|
||||
("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Συνέχεια με {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -262,5 +262,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."),
|
||||
("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."),
|
||||
("note-at-conn-end-tip", "Ask for note at end of connection"),
|
||||
("rel-mouse-not-supported-peer-tip", "Relative Mouse Mode is not supported by the connected peer."),
|
||||
("rel-mouse-not-ready-tip", "Relative Mouse Mode is not ready yet. Please try again."),
|
||||
("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."),
|
||||
("rel-mouse-exit-{}-tip", "Press {} to exit."),
|
||||
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."),
|
||||
("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"),
|
||||
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", ""),
|
||||
("This PC", ""),
|
||||
("or", ""),
|
||||
("Continue with", ""),
|
||||
("Elevate", ""),
|
||||
("Zoom cursor", ""),
|
||||
("Accept sessions via password", ""),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Mostrar RustDesk"),
|
||||
("This PC", "Este PC"),
|
||||
("or", "o"),
|
||||
("Continue with", "Continuar con"),
|
||||
("Elevate", "Elevar privilegios"),
|
||||
("Zoom cursor", "Ampliar cursor"),
|
||||
("Accept sessions via password", "Aceptar sesiones a través de contraseña"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Continuar con {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Kuva RustDesk"),
|
||||
("This PC", "See arvuti"),
|
||||
("or", "või"),
|
||||
("Continue with", "Jätka koos"),
|
||||
("Elevate", "Tõsta"),
|
||||
("Zoom cursor", "Suumi kursorit"),
|
||||
("Accept sessions via password", "Aktsepteeri seansid parooli kaudu"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Jätka koos {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Erakutsi RustDesk"),
|
||||
("This PC", "PC hau"),
|
||||
("or", "edo"),
|
||||
("Continue with", "Jarraitu honekin"),
|
||||
("Elevate", "Igo maila"),
|
||||
("Zoom cursor", "Handitu kurtsorea"),
|
||||
("Accept sessions via password", "Onartu saioak pasahitzaren bidez"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "{} honekin jarraitu"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "RustDesk نمایش"),
|
||||
("This PC", "This PC"),
|
||||
("or", "یا"),
|
||||
("Continue with", "ادامه با"),
|
||||
("Elevate", "ارتقاء"),
|
||||
("Zoom cursor", " بزرگنمایی نشانگر ماوس"),
|
||||
("Accept sessions via password", "قبول درخواست با رمز عبور"),
|
||||
@@ -695,19 +694,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("View camera", "نمایش دوربین"),
|
||||
("Enable camera", "فعال کردن دوربین"),
|
||||
("No cameras", "هیچ دوربینی یافت نشد"),
|
||||
("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمیشود"),
|
||||
("view_camera_unsupported_tip", "ریموت از مشاهده دوربین پشتیبانی نمی کند."),
|
||||
("Terminal", "ترمینال"),
|
||||
("Enable terminal", "فعالسازی ترمینال"),
|
||||
("New tab", "زبانه جدید"),
|
||||
("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"),
|
||||
("Terminal (Run as administrator)", "ترمینال (اجرای به عنوان مدیر سیستم)"),
|
||||
("terminal-admin-login-tip", "برای اجرای ترمینال بهعنوان مدیر، نام کاربری و رمز عبور مدیر سیستم را وارد کنید."),
|
||||
("terminal-admin-login-tip", "برای اجرای ترمینال به عنوان مدیر، نام کاربری و رمز عبور مدیر سیستم ریموت را وارد کنید."),
|
||||
("Failed to get user token.", "دریافت توکن کاربر ناموفق بود."),
|
||||
("Incorrect username or password.", "نام کاربری یا رمز عبور اشتباه است."),
|
||||
("The user is not an administrator.", "کاربر دارای دسترسی مدیر سیستم نیست."),
|
||||
("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموفق بود."),
|
||||
("Supported only in the installed version.", "فقط در نسخه نصبشده پشتیبانی میشود."),
|
||||
("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."),
|
||||
("Supported only in the installed version.", "فقط در نسخه نصب شده پشتیبانی میشود."),
|
||||
("elevation_username_tip", "وارد نمایید domain\\username یا username نام کاربری را به صورت"),
|
||||
("Preparing for installation ...", "در حال آمادهسازی برای نصب..."),
|
||||
("Show my cursor", "نمایش نشانگر من"),
|
||||
("Scale custom", "مقیاس سفارشی"),
|
||||
@@ -719,15 +718,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Small", "کوچک"),
|
||||
("Large", "بزرگ"),
|
||||
("Show virtual joystick", "نمایش جویاستیک مجازی"),
|
||||
("Edit note", ""),
|
||||
("Alias", ""),
|
||||
("Edit note", "ویرایش یادداشت"),
|
||||
("Alias", "نام مستعار"),
|
||||
("ScrollEdge", ""),
|
||||
("Allow insecure TLS fallback", ""),
|
||||
("allow-insecure-tls-fallback-tip", ""),
|
||||
("Disable UDP", ""),
|
||||
("disable-udp-tip", ""),
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Allow insecure TLS fallback", "استفاده از TLS غیر امن در ارتباط"),
|
||||
("allow-insecure-tls-fallback-tip", "بهطور پیشفرض، RustDesk گواهی سرور را برای پروتکلها با استفاده از TLS تأیید میکند.\nبا فعال بودن این گزینه، RustDesk دوباره مرحله تأیید را رد میکند و در صورت عدم موفقیت تأیید ادامه میدهد."),
|
||||
("Disable UDP", "UDP غیر فعال کردن"),
|
||||
("disable-udp-tip", "کنترل می کند که آیا فقط از TCP استفاده شود یا خیر.\nوقتی این گزینه فعال باشد، RustDesk دیگر از UDP 21116 استفاده نمی کند، به جای آن از TCP 21116 استفاده می شود."),
|
||||
("server-oss-not-support-tip", "توجه: سرور RustDesk OSS این ویژگی را ندارد."),
|
||||
("input note here", "یادداشت را اینجا وارد کنید"),
|
||||
("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "ادامه با {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Näytä RustDesk"),
|
||||
("This PC", "Tämä tietokone"),
|
||||
("or", "tai"),
|
||||
("Continue with", "Jatka käyttäen"),
|
||||
("Elevate", "Korota oikeudet"),
|
||||
("Zoom cursor", "Suurennusosoitin"),
|
||||
("Accept sessions via password", "Hyväksy istunnot salasanalla"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Jatka käyttäen {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"),
|
||||
("Do this for all conflicts", "Appliquer à tous les conflits"),
|
||||
("This is irreversible!", "Ceci est irréversible !"),
|
||||
("This is irreversible!", "Cette action est irréversible !"),
|
||||
("Deleting", "Suppression"),
|
||||
("files", "fichiers"),
|
||||
("Waiting", "En attente"),
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Afficher RustDesk"),
|
||||
("This PC", "Ce PC"),
|
||||
("or", "ou"),
|
||||
("Continue with", "Continuer avec"),
|
||||
("Elevate", "Élever les privilèges"),
|
||||
("Zoom cursor", "Augmenter la taille du curseur"),
|
||||
("Accept sessions via password", "Accepter les sessions via mot de passe"),
|
||||
@@ -728,6 +727,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("disable-udp-tip", "Contrôle l’utilisation exclusive du mode TCP.\nLorsque cette option est activée, RustDesk n’utilise plus le port UDP 21116 et utilise le port TCP 21116 à la place."),
|
||||
("server-oss-not-support-tip", "Note : Cette fonctionnalité n’est pas disponible sous la version open-source du serveur RustDesk."),
|
||||
("input note here", "saisir la note ici"),
|
||||
("note-at-conn-end-tip", "Proposer d’écrire une note une fois la connexion terminée"),
|
||||
("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"),
|
||||
("Show terminal extra keys", "Afficher les touches supplémentaires du terminal"),
|
||||
("Relative mouse mode", "Mode souris relative"),
|
||||
("rel-mouse-not-supported-peer-tip", "Le mode souris relative n’est pas pris en charge par l’appareil distant."),
|
||||
("rel-mouse-not-ready-tip", "Le mode souris relative n’est pas encore prêt ; veuillez réessayer."),
|
||||
("rel-mouse-lock-failed-tip", "Échec du verrouillage du curseur. Le mode souris relative a été désactivé."),
|
||||
("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."),
|
||||
("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."),
|
||||
("Changelog", "Journal des modifications"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"),
|
||||
("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"),
|
||||
("Continue with {}", "Continuer avec {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "RustDesk-ის ჩვენება"),
|
||||
("This PC", "ეს კომპიუტერი"),
|
||||
("or", "ან"),
|
||||
("Continue with", "გაგრძელება"),
|
||||
("Elevate", "უფლებების აწევა"),
|
||||
("Zoom cursor", "კურსორის მასშტაბირება"),
|
||||
("Accept sessions via password", "სესიების მიღება პაროლით"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "{}-ით გაგრძელება"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "הצג את RustDesk"),
|
||||
("This PC", "מחשב זה"),
|
||||
("or", "או"),
|
||||
("Continue with", "המשך עם"),
|
||||
("Elevate", "הפעל הרשאות מורחבות"),
|
||||
("Zoom cursor", "הגדל סמן"),
|
||||
("Accept sessions via password", "קבל הפעלות באמצעות סיסמה"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "המשך עם {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Prikaži RustDesk"),
|
||||
("This PC", "Ovo računalo"),
|
||||
("or", "ili"),
|
||||
("Continue with", "Nastavi sa"),
|
||||
("Elevate", "Izdigni"),
|
||||
("Zoom cursor", "Zumiraj kursor"),
|
||||
("Accept sessions via password", "Prihvati sesije preko lozinke"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Nastavi sa {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
122
src/lang/hu.rs
122
src/lang/hu.rs
@@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Enable file transfer", "Fájlátvitel engedélyezése"),
|
||||
("Enable TCP tunneling", "TCP-alagút engedélyezése"),
|
||||
("IP Whitelisting", "IP engedélyezési lista"),
|
||||
("ID/Relay Server", "ID/Továbbító-kiszolgáló"),
|
||||
("ID/Relay Server", "Azonosító-/Továbbító-kiszolgáló"),
|
||||
("Import server config", "Kiszolgáló-konfiguráció importálása"),
|
||||
("Export Server Config", "Kiszolgáló-konfiguráció exportálása"),
|
||||
("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"),
|
||||
@@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Clipboard is empty", "A vágólap üres"),
|
||||
("Stop service", "Szolgáltatás leállítása"),
|
||||
("Change ID", "Azonosító módosítása"),
|
||||
("Your new ID", "Az új azonosító"),
|
||||
("Your new ID", "Új azonosító"),
|
||||
("length %min% to %max%", "hossz %min% és %max% között"),
|
||||
("starts with a letter", "betűvel kezdődik"),
|
||||
("allowed characters", "engedélyezett karakterek"),
|
||||
@@ -50,11 +50,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Build Date", "Összeállítás ideje"),
|
||||
("Version", "Verzió"),
|
||||
("Home", "Kezdőképernyő"),
|
||||
("Audio Input", "Hangátvitel"),
|
||||
("Audio Input", "Hangbemenet"),
|
||||
("Enhancements", "Fejlesztések"),
|
||||
("Hardware Codec", "Hardveres kodek"),
|
||||
("Adaptive bitrate", "Adaptív bitráta"),
|
||||
("ID Server", "ID-kiszolgáló"),
|
||||
("ID Server", "Azonosító-kiszolgáló"),
|
||||
("Relay Server", "Továbbító-kiszolgáló"),
|
||||
("API Server", "API-kiszolgáló"),
|
||||
("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."),
|
||||
@@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Optimize reaction time", "Gyorsan reagáló"),
|
||||
("Custom", "Egyéni"),
|
||||
("Show remote cursor", "Távoli kurzor megjelenítése"),
|
||||
("Show quality monitor", "Kijelző minőségének ellenőrzése"),
|
||||
("Show quality monitor", "Kapcsolat minőségének megjelenítése"),
|
||||
("Disable clipboard", "Közös vágólap kikapcsolása"),
|
||||
("Lock after session end", "Távoli fiók zárolása a munkamenet végén"),
|
||||
("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del billentyűzetkombinációt"),
|
||||
@@ -148,8 +148,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használatakor. A megfelelő működés érdekében, telepítse a RustDesk alkalmazást a számítógépére."),
|
||||
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
|
||||
("Configure", "Beállítás"),
|
||||
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell biztosítania."),
|
||||
("config_screen", "Ahhoz, hogy távolról hozzáférhessen számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."),
|
||||
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
|
||||
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
|
||||
("Installing ...", "Telepítés…"),
|
||||
("Install", "Telepítés"),
|
||||
("Installation", "Telepítés"),
|
||||
@@ -159,7 +159,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc szerződés."),
|
||||
("Accept and Install", "Elfogadás és telepítés"),
|
||||
("End-user license agreement", "Végfelhasználói licenc szerződés"),
|
||||
("Generating ...", "Létrehozás…"),
|
||||
("Generating ...", "Előállítás…"),
|
||||
("Your installation is lower version.", "A telepített verzió alacsonyabb."),
|
||||
("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"),
|
||||
("Listening ...", "Figyelés…"),
|
||||
@@ -177,7 +177,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Accept", "Elfogadás"),
|
||||
("Dismiss", "Elutasítás"),
|
||||
("Disconnect", "Kapcsolat bontása"),
|
||||
("Enable file copy and paste", "Fájlok másolásának és beillesztésének engedélyezése"),
|
||||
("Enable file copy and paste", "Fájlmásolás és -beillesztés engedélyezése"),
|
||||
("Connected", "Kapcsolódva"),
|
||||
("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"),
|
||||
("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"),
|
||||
@@ -220,7 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Logout", "Kilépés"),
|
||||
("Tags", "Címkék"),
|
||||
("Search ID", "Azonosító keresése…"),
|
||||
("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"),
|
||||
("whitelist_sep", "A címeket vesszővel, pontosvesszővel, szóközzel vagy új sorral kell elválasztani"),
|
||||
("Add ID", "Azonosító hozzáadása"),
|
||||
("Add Tag", "Címke hozzáadása"),
|
||||
("Unselect all tags", "A címkék kijelölésének megszüntetése"),
|
||||
@@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Socks5 Proxy", "Socks5 Proxy"),
|
||||
("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"),
|
||||
("Discovered", "Felfedezett"),
|
||||
("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"),
|
||||
("install_daemon_tip", "Automatikus indításhoz szükséges a szolgáltatás telepítése"),
|
||||
("Remote ID", "Távoli azonosító"),
|
||||
("Paste", "Beillesztés"),
|
||||
("Paste here?", "Beillesztés ide?"),
|
||||
@@ -258,10 +258,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Three-Finger vertically", "Három ujj függőlegesen"),
|
||||
("Mouse Wheel", "Egérgörgő"),
|
||||
("Two-Finger Move", "Kétujjas mozgatás"),
|
||||
("Canvas Move", "Nézet mozgatása"),
|
||||
("Canvas Move", "Vászon mozgatása"),
|
||||
("Pinch to Zoom", "Kétujjas nagyítás"),
|
||||
("Canvas Zoom", "Nézet nagyítása"),
|
||||
("Reset canvas", "Nézet visszaállítása"),
|
||||
("Canvas Zoom", "Vászon nagyítása"),
|
||||
("Reset canvas", "Vászon visszaállítása"),
|
||||
("No permission of file transfer", "Nincs engedély a fájlátvitelre"),
|
||||
("Note", "Megjegyzés"),
|
||||
("Connection", "Kapcsolat"),
|
||||
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Do you accept?", "Elfogadás?"),
|
||||
("Open System Setting", "Rendszerbeállítások megnyitása"),
|
||||
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
|
||||
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."),
|
||||
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
|
||||
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
|
||||
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
|
||||
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
|
||||
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
|
||||
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
|
||||
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."),
|
||||
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
|
||||
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
|
||||
("Account", "Fiók"),
|
||||
("Overwrite", "Felülírás"),
|
||||
@@ -374,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Disconnected", "Kapcsolat bontva"),
|
||||
("Other", "Egyéb"),
|
||||
("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"),
|
||||
("Keyboard Settings", "Billentyűzet beállítások"),
|
||||
("Keyboard Settings", "Billentyűzet-beállítások"),
|
||||
("Full Access", "Teljes hozzáférés"),
|
||||
("Screen Share", "Képernyőmegosztás"),
|
||||
("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."),
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "A RustDesk megjelenítése"),
|
||||
("This PC", "Ez a számítógép"),
|
||||
("or", "vagy"),
|
||||
("Continue with", "Folytatás a következővel"),
|
||||
("Elevate", "Hozzáférés engedélyezése"),
|
||||
("Zoom cursor", "Kurzor nagyítása"),
|
||||
("Accept sessions via password", "Munkamenetek elfogadása jelszóval"),
|
||||
@@ -408,15 +407,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
|
||||
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
|
||||
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
|
||||
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."),
|
||||
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."),
|
||||
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
|
||||
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
|
||||
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
|
||||
("Wait", "Várjon"),
|
||||
("Elevation Error", "Emelt szintű hozzáférési hiba"),
|
||||
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
|
||||
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
|
||||
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
|
||||
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
|
||||
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
|
||||
("Request Elevation", "Emelt szintű jogok igénylése"),
|
||||
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
|
||||
("Elevate successfully", "Emelt szintű jogok megadva"),
|
||||
@@ -442,19 +441,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Voice call", "Hanghívás"),
|
||||
("Text chat", "Szöveges csevegés"),
|
||||
("Stop voice call", "Hanghívás leállítása"),
|
||||
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
|
||||
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
|
||||
("Reconnect", "Újrakapcsolódás"),
|
||||
("Codec", "Kodek"),
|
||||
("Resolution", "Felbontás"),
|
||||
("No transfers in progress", "Nincs folyamatban átvitel"),
|
||||
("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"),
|
||||
("RDP Settings", "RDP beállítások"),
|
||||
("RDP Settings", "RDP-beállítások"),
|
||||
("Sort by", "Rendezés"),
|
||||
("New Connection", "Új kapcsolat"),
|
||||
("Restore", "Visszaállítás"),
|
||||
("Minimize", "Minimalizálás"),
|
||||
("Maximize", "Maximalizálás"),
|
||||
("Your Device", "Az Ön eszköze"),
|
||||
("Your Device", "Saját eszköz"),
|
||||
("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."),
|
||||
("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és adja hozzá a kedvencekhez!"),
|
||||
("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."),
|
||||
@@ -469,7 +468,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"),
|
||||
("remember_account_tip", "Emlékezzen erre a fiókra"),
|
||||
("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."),
|
||||
("OS Account", "OS fiók"),
|
||||
("OS Account", "OS-fiók"),
|
||||
("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."),
|
||||
("another_user_login_text_tip", "Különálló"),
|
||||
("xorg_not_found_title_tip", "Xorg nem található."),
|
||||
@@ -490,7 +489,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Update", "Frissítés"),
|
||||
("Enable", "Engedélyezés"),
|
||||
("Disable", "Letiltás"),
|
||||
("Options", "Beállítások"),
|
||||
("Options", "Opciók"),
|
||||
("resolution_original_tip", "Eredeti felbontás"),
|
||||
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
|
||||
("resolution_custom_tip", "Testre szabható felbontás"),
|
||||
@@ -515,7 +514,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Already exists", "Már létezik"),
|
||||
("Change Password", "Jelszó módosítása"),
|
||||
("Refresh Password", "Jelszó frissítése"),
|
||||
("ID", "ID"),
|
||||
("ID", "Azonosító"),
|
||||
("Grid View", "Mozaik nézet"),
|
||||
("List View", "Lista nézet"),
|
||||
("Select", "Kiválasztás"),
|
||||
@@ -543,13 +542,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Frissítse a RustDesk Server Prot a(z) {} vagy újabb verzióra!"),
|
||||
("pull_group_failed_tip", "A csoport frissítése nem sikerült"),
|
||||
("Filter by intersection", "Szűrés metszéspontok szerint"),
|
||||
("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövő munkamenetek közben"),
|
||||
("Remove wallpaper during incoming sessions", "Háttérkép eltávolítása bejövő munkameneteknél"),
|
||||
("Test", "Teszt"),
|
||||
("display_is_plugged_out_msg", "A képernyő nincs csatlakoztatva, váltson az első képernyőre."),
|
||||
("No displays", "Nincsenek kijelzők"),
|
||||
("Open in new window", "Megnyitás új ablakban"),
|
||||
("Show displays as individual windows", "Kijelzők megjelenítése egyedi ablakokként"),
|
||||
("Use all my displays for the remote session", "Az összes kijelzőm használata a távoli munkamenethez"),
|
||||
("Use all my displays for the remote session", "Összes kijelző használata a távoli munkamenethez"),
|
||||
("selinux_tip", "A SELinux engedélyezve van az eszközén, ami azt okozhatja, hogy a RustDesk nem fut megfelelően, mint ellenőrzött."),
|
||||
("Change view", "Nézet módosítása"),
|
||||
("Big tiles", "Nagy csempék"),
|
||||
@@ -559,7 +558,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Plug out all", "Kapcsolja ki az összeset"),
|
||||
("True color (4:4:4)", "Valódi szín (4:4:4)"),
|
||||
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
|
||||
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „<id>@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."),
|
||||
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "1. mód"),
|
||||
("privacy_mode_impl_virtual_display_tip", "2. mód"),
|
||||
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
|
||||
@@ -569,7 +568,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("input_source_2_tip", "2. bemeneti forrás"),
|
||||
("Swap control-command key", "Vezérlő- és parancsgombok cseréje"),
|
||||
("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"),
|
||||
("2FA code", "2FA kód"),
|
||||
("2FA code", "2FA-kód"),
|
||||
("More", "Továbbiak"),
|
||||
("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"),
|
||||
("enable-2fa-desc", "Állítsa be a hitelesítőt. Használhat egy hitelesítő alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."),
|
||||
@@ -604,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Outgoing", "Kimenő"),
|
||||
("Clear Wayland screen selection", "Wayland képernyő kiválasztásának törlése"),
|
||||
("clear_Wayland_screen_selection_tip", "A képernyőválasztás törlése után újra kiválaszthatja a megosztandó képernyőt."),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Biztos, hogy törölni szeretné a Wayland képernyő kiválasztását?"),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Biztosan törölni szeretné a Wayland képernyő kiválasztását?"),
|
||||
("android_new_voice_call_tip", "Új hanghívás-kérés érkezett. Ha elfogadja a megkeresést, a hang átvált hangkommunikációra."),
|
||||
("texture_render_tip", "Használja a textúra leképezést a képek simábbá tételéhez. Ezt az opciót kikapcsolhatja, ha leképezési problémái vannak."),
|
||||
("Use texture rendering", "Textúra leképezés használata"),
|
||||
@@ -619,11 +618,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Apps", "Alkalmazások"),
|
||||
("Volume up", "Hangerő fel"),
|
||||
("Volume down", "Hangerő le"),
|
||||
("Power", "Teljesítmény"),
|
||||
("Power", "Főkapcsoló"),
|
||||
("Telegram bot", "Telegram bot"),
|
||||
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
|
||||
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
|
||||
("cancel-2fa-confirm-tip", "Biztosan le akarja mondani a 2FA-t?"),
|
||||
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
|
||||
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
|
||||
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
|
||||
("About RustDesk", "A RustDesk névjegye"),
|
||||
("Send clipboard keystrokes", "Billentyűleütések küldése a vágólapra"),
|
||||
@@ -643,18 +642,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
|
||||
("Authentication Required", "Hitelesítés szükséges"),
|
||||
("Authenticate", "Hitelesítés"),
|
||||
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „<id>@public” betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
|
||||
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
|
||||
("Download", "Letöltés"),
|
||||
("Upload folder", "Mappa feltöltése"),
|
||||
("Upload files", "Fájlok feltöltése"),
|
||||
("Clipboard is synchronized", "A vágólap szinkronizálva van"),
|
||||
("Update client clipboard", "Az ügyfél vágólapjának frissítése"),
|
||||
("Update client clipboard", "Kliens vágólapjának frissítése"),
|
||||
("Untagged", "Címkézetlen"),
|
||||
("new-version-of-{}-tip", "A(z) {} új verziója"),
|
||||
("Accessible devices", "Hozzáférhető eszközök"),
|
||||
("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"),
|
||||
("d3d_render_tip", "D3D leképezés"),
|
||||
("Use D3D rendering", "D3D leképezés használata"),
|
||||
("d3d_render_tip", "D3D-leképezés"),
|
||||
("Use D3D rendering", "D3D-leképezés használata"),
|
||||
("Printer", "Nyomtató"),
|
||||
("printer-os-requirement-tip", "Nyomtató operációs rendszerének minimális rendszerkövetelménye"),
|
||||
("printer-requires-installed-{}-client-tip", "A nyomtatóhoz szükséges a(z) {} kliens telepítése"),
|
||||
@@ -679,12 +678,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Save as", "Mentés másként"),
|
||||
("Copy to clipboard", "Másolás a vágólapra"),
|
||||
("Enable remote printer", "Távoli nyomtatók engedélyezése"),
|
||||
("Downloading {}", "Letöltés {}"),
|
||||
("{} Update", "{} Frissítés"),
|
||||
("{}-to-update-tip", "A {} bezárása és az új verzió telepítése."),
|
||||
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
|
||||
("Downloading {}", "{} letöltése"),
|
||||
("{} Update", "{} frissítés"),
|
||||
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
|
||||
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
|
||||
("Auto update", "Automatikus frissítés"),
|
||||
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kérjük, kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
|
||||
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
|
||||
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
|
||||
("Use WebSocket", "WebSocket használata"),
|
||||
("Trackpad speed", "Érintőpad sebessége"),
|
||||
@@ -701,14 +700,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("New tab", "Új lap"),
|
||||
("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"),
|
||||
("Terminal (Run as administrator)", "Terminál (rendszergazdaként futtatva)"),
|
||||
("terminal-admin-login-tip", "Kérjük, adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."),
|
||||
("terminal-admin-login-tip", "Adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."),
|
||||
("Failed to get user token.", "Hiba a felhasználói token lekérdezésekor."),
|
||||
("Incorrect username or password.", "A felhasználónév vagy a jelszó helytelen."),
|
||||
("The user is not an administrator.", "A felhasználó nem rendszergazda."),
|
||||
("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."),
|
||||
("Supported only in the installed version.", "Csak a telepített változatban támogatott."),
|
||||
("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"),
|
||||
("Preparing for installation ...", "Felkészülés a telepítésre ..."),
|
||||
("elevation_username_tip", "Felhasználónév vagy tartománynév megadása"),
|
||||
("Preparing for installation ...", "Felkészülés a telepítésre…"),
|
||||
("Show my cursor", "Kurzor megjelenítése"),
|
||||
("Scale custom", "Egyéni méretarány"),
|
||||
("Custom scale slider", "Egyéni méretarány-csúszka"),
|
||||
@@ -719,15 +718,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Small", "Kicsi"),
|
||||
("Large", "Nagy"),
|
||||
("Show virtual joystick", "Virtuális vezérlő megjelenítése"),
|
||||
("Edit note", "Jegyzet szerkesztése"),
|
||||
("Edit note", "Megjegyzés szerkesztése"),
|
||||
("Alias", "Álnév"),
|
||||
("ScrollEdge", ""),
|
||||
("Allow insecure TLS fallback", ""),
|
||||
("allow-insecure-tls-fallback-tip", ""),
|
||||
("Disable UDP", ""),
|
||||
("disable-udp-tip", ""),
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("ScrollEdge", "Görgetés az ablak szélein"),
|
||||
("Allow insecure TLS fallback", "Nem biztonságos TLS-tartalék engedélyezése"),
|
||||
("allow-insecure-tls-fallback-tip", "Alapértelmezés szerint a RustDesk ellenőrzi a kiszolgáló tanúsítványát a TLS-protokollok esetében. Ha ez a beállítás engedélyezve van, a RustDesk kihagyja az ellenőrzési lépést, és az ellenőrzés sikertelensége esetén folytatja a műveletet."),
|
||||
("Disable UDP", "UDP letiltása"),
|
||||
("disable-udp-tip", "Meghatározza, hogy csak TCP-t használjon-e. Ha ez az beállítás engedélyezve van, a RustDesk nem fogja többé használni a 21116-os UDP-portot, helyette a 21116-os TCP-portot fogja használni."),
|
||||
("server-oss-not-support-tip", "MEGJEGYZÉS: Az OSS RustDesk kiszolgáló nem támogatja ezt a funkciót."),
|
||||
("input note here", "Megjegyzés beírása"),
|
||||
("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"),
|
||||
("Show terminal extra keys", "További terminálgombok megjelenítése"),
|
||||
("Relative mouse mode", "Relatív egér mód"),
|
||||
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
|
||||
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
|
||||
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
|
||||
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
|
||||
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
|
||||
("Changelog", "Változáslista"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
|
||||
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
|
||||
("Continue with {}", "Folytatás a következővel: {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Tampilkan RustDesk"),
|
||||
("This PC", "PC ini"),
|
||||
("or", "atau"),
|
||||
("Continue with", "Lanjutkan dengan"),
|
||||
("Elevate", "Elevasi"),
|
||||
("Zoom cursor", "Perbersar Kursor"),
|
||||
("Accept sessions via password", "Izinkan sesi dengan kata sandi"),
|
||||
@@ -729,5 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("Continue with {}", "Lanjutkan dengan {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -102,9 +102,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Unselect All", "Deseleziona tutto"),
|
||||
("Empty Directory", "Cartella vuota"),
|
||||
("Not an empty directory", "Non è una cartella vuota"),
|
||||
("Are you sure you want to delete this file?", "Sei sicuro di voler eliminare questo file?"),
|
||||
("Are you sure you want to delete this empty directory?", "Sei sicuro di voler eliminare questa cartella vuota?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Sei sicuro di voler eliminare il file di questa cartella?"),
|
||||
("Are you sure you want to delete this file?", "Vuoi eliminare questo file?"),
|
||||
("Are you sure you want to delete this empty directory?", "Vuoi eliminare questa cartella vuota?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Vuoi eliminare il file di questa cartella?"),
|
||||
("Do this for all conflicts", "Ricorda questa scelta per tutti i conflitti"),
|
||||
("This is irreversible!", "Questo è irreversibile!"),
|
||||
("Deleting", "Eliminazione di"),
|
||||
@@ -243,7 +243,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Remote ID", "ID remoto"),
|
||||
("Paste", "Incolla"),
|
||||
("Paste here?", "Incollare qui?"),
|
||||
("Are you sure to close the connection?", "Sei sicuro di voler chiudere la connessione?"),
|
||||
("Are you sure to close the connection?", "Vuoi chiudere la connessione?"),
|
||||
("Download new version", "Scarica nuova versione"),
|
||||
("Touch mode", "Modalità tocco"),
|
||||
("Mouse mode", "Modalità mouse"),
|
||||
@@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Set permanent password", "Imposta password permanente"),
|
||||
("Enable remote restart", "Abilita riavvio da remoto"),
|
||||
("Restart remote device", "Riavvia dispositivo remoto"),
|
||||
("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
|
||||
("Are you sure you want to restart", "Vuoi riavviare?"),
|
||||
("Restarting remote device", "Il dispositivo remoto si sta riavviando"),
|
||||
("remote_restarting_tip", "Riavvia il dispositivo remoto"),
|
||||
("Copied", "Copiato"),
|
||||
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Show RustDesk", "Visualizza RustDesk"),
|
||||
("This PC", "Questo PC"),
|
||||
("or", "O"),
|
||||
("Continue with", "Continua con"),
|
||||
("Elevate", "Eleva"),
|
||||
("Zoom cursor", "Cursore zoom"),
|
||||
("Accept sessions via password", "Accetta sessioni via password"),
|
||||
@@ -502,7 +501,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Outgoing connection", "Connessioni in uscita"),
|
||||
("Exit", "Esci da RustDesk"),
|
||||
("Open", "Apri RustDesk"),
|
||||
("logout_tip", "Sei sicuro di voler uscire?"),
|
||||
("logout_tip", "Vuoi disconnetterti?"),
|
||||
("Service", "Servizio"),
|
||||
("Start", "Avvia"),
|
||||
("Stop", "Ferma"),
|
||||
@@ -604,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Outgoing", "In uscita"),
|
||||
("Clear Wayland screen selection", "Annulla selezione schermata Wayland"),
|
||||
("clear_Wayland_screen_selection_tip", "Dopo aver annullato la selezione schermo, è possibile selezionare nuovamente lo schermo da condividere."),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Sei sicuro di voler annullare la selezione schermo Wayland?"),
|
||||
("confirm_clear_Wayland_screen_selection_tip", "Vuoi annullare la selezione schermo Wayland?"),
|
||||
("android_new_voice_call_tip", "È stata ricevuta una nuova richiesta di chiamata vocale. Se accetti, l'audio passerà alla comunicazione vocale."),
|
||||
("texture_render_tip", "Usa il rendering texture per rendere le immagini più fluide. Se riscontri problemi di rendering prova a disabilitare questa opzione."),
|
||||
("Use texture rendering", "Usa rendering texture"),
|
||||
@@ -623,8 +622,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Telegram bot", "Bot Telegram"),
|
||||
("enable-bot-tip", "Se abiliti questa funzione, puoi ricevere il codice 2FA dal tuo bot.\nPuò anche funzionare come notifica di connessione."),
|
||||
("enable-bot-desc", "1. apri una chat con @BotFather.\n2. Invia il comando \"/newbot\", dopo aver completato questo passaggio riceverai un token.\n3. Avvia una chat con il tuo bot appena creato. Per attivarlo Invia un messaggio che inizia con una barra (\"/\") tipo \"/hello\".\n"),
|
||||
("cancel-2fa-confirm-tip", "Sei sicuro di voler annullare 2FA?"),
|
||||
("cancel-bot-confirm-tip", "Sei sicuro di voler annullare Telegram?"),
|
||||
("cancel-2fa-confirm-tip", "Vuoi disabilitare 2FA?"),
|
||||
("cancel-bot-confirm-tip", "Vuoi disabilitare il bot Telegram?"),
|
||||
("About RustDesk", "Info su RustDesk"),
|
||||
("Send clipboard keystrokes", "Invia sequenze tasti appunti"),
|
||||
("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."),
|
||||
@@ -726,8 +725,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("allow-insecure-tls-fallback-tip", "Per impostazione predefinita, RustDesk verifica il certificato del server per i protocolli usando TLS.\nCon questa opzione abilitata, RustDesk salterà il passaggio di verifica e procederà in caso di errore di verifica."),
|
||||
("Disable UDP", "Disabilita UDP"),
|
||||
("disable-udp-tip", "Controlla se usare solo TCP.\nQuando questa opzione è abilitata, RustDesk non userà più UDP 21116, verrà invece usato TCP 21116."),
|
||||
("server-oss-not-support-tip", "NOTA: il sistema operativo del server RustDesk non include questa funzionalità."),
|
||||
("server-oss-not-support-tip", "Nota: il sistema operativo del server RustDesk non include questa funzionalità."),
|
||||
("input note here", "Inserisci nota qui"),
|
||||
("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"),
|
||||
("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"),
|
||||
("Relative mouse mode", "Modalità relativa mouse"),
|
||||
("rel-mouse-not-supported-peer-tip", "La modalità mouse relativa non è supportata dal peer connesso."),
|
||||
("rel-mouse-not-ready-tip", "La modalità mouse relativa non è ancora pronta. Riprova."),
|
||||
("rel-mouse-lock-failed-tip", "Impossibile bloccare il cursore. La modalità mouse relativa è stata disabilitata."),
|
||||
("rel-mouse-exit-{}-tip", "Premi {} per uscire."),
|
||||
("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."),
|
||||
("Changelog", "Novità programma"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
|
||||
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
|
||||
("Continue with {}", "Continua con {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user